summaryrefslogtreecommitdiffstats
path: root/routers
diff options
context:
space:
mode:
authorguillep2k <18600385+guillep2k@users.noreply.github.com>2020-03-02 15:25:36 -0300
committerGitHub <noreply@github.com>2020-03-02 15:25:36 -0300
commit5e1438ba92fe5b4398ebf468e4ede21c7ef60409 (patch)
tree40096ee69a4ac5df46428a3cf1ae7ee3bee8078e /routers
parentb5ecc82d6e22b5701bfadc1ebc430b9c7fef0cc8 (diff)
downloadgitea-5e1438ba92fe5b4398ebf468e4ede21c7ef60409.tar.gz
gitea-5e1438ba92fe5b4398ebf468e4ede21c7ef60409.zip
Admin page for managing user e-mail activation (#10557)
* Implement mail activation admin panel * Add export comments * Fix another export comment * again... * And again! * Apply suggestions by @lunny * Add UI for user activated emails * Make new activation UI work * Fix lint * Prevent admin from self-deactivate; add modal Co-authored-by: zeripath <art27@cantab.net>
Diffstat (limited to 'routers')
-rw-r--r--routers/admin/emails.go157
-rw-r--r--routers/routes/routes.go5
-rw-r--r--routers/user/auth.go10
-rw-r--r--routers/user/setting/account.go62
4 files changed, 232 insertions, 2 deletions
diff --git a/routers/admin/emails.go b/routers/admin/emails.go
new file mode 100644
index 0000000000..f0b14ce5e5
--- /dev/null
+++ b/routers/admin/emails.go
@@ -0,0 +1,157 @@
+// Copyright 2020 The Gitea Authors.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package admin
+
+import (
+ "bytes"
+ "net/url"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/unknwon/com"
+)
+
+const (
+ tplEmails base.TplName = "admin/emails/list"
+)
+
+// Emails show all emails
+func Emails(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("admin.emails")
+ ctx.Data["PageIsAdmin"] = true
+ ctx.Data["PageIsAdminEmails"] = true
+
+ opts := &models.SearchEmailOptions{
+ ListOptions: models.ListOptions{
+ PageSize: setting.UI.Admin.UserPagingNum,
+ Page: ctx.QueryInt("page"),
+ },
+ }
+
+ if opts.Page <= 1 {
+ opts.Page = 1
+ }
+
+ type ActiveEmail struct {
+ models.SearchEmailResult
+ CanChange bool
+ }
+
+ var (
+ baseEmails []*models.SearchEmailResult
+ emails []ActiveEmail
+ count int64
+ err error
+ orderBy models.SearchEmailOrderBy
+ )
+
+ ctx.Data["SortType"] = ctx.Query("sort")
+ switch ctx.Query("sort") {
+ case "email":
+ orderBy = models.SearchEmailOrderByEmail
+ case "reverseemail":
+ orderBy = models.SearchEmailOrderByEmailReverse
+ case "username":
+ orderBy = models.SearchEmailOrderByName
+ case "reverseusername":
+ orderBy = models.SearchEmailOrderByNameReverse
+ default:
+ ctx.Data["SortType"] = "email"
+ orderBy = models.SearchEmailOrderByEmail
+ }
+
+ opts.Keyword = ctx.QueryTrim("q")
+ opts.SortType = orderBy
+ if len(ctx.Query("is_activated")) != 0 {
+ opts.IsActivated = util.OptionalBoolOf(ctx.QueryBool("activated"))
+ }
+ if len(ctx.Query("is_primary")) != 0 {
+ opts.IsPrimary = util.OptionalBoolOf(ctx.QueryBool("primary"))
+ }
+
+ if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
+ baseEmails, count, err = models.SearchEmails(opts)
+ if err != nil {
+ ctx.ServerError("SearchEmails", err)
+ return
+ }
+ emails = make([]ActiveEmail, len(baseEmails))
+ for i := range baseEmails {
+ emails[i].SearchEmailResult = *baseEmails[i]
+ // Don't let the admin deactivate its own primary email address
+ // We already know the user is admin
+ emails[i].CanChange = ctx.User.ID != emails[i].UID || !emails[i].IsPrimary
+ }
+ }
+ ctx.Data["Keyword"] = opts.Keyword
+ ctx.Data["Total"] = count
+ ctx.Data["Emails"] = emails
+
+ pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+ pager.SetDefaultParams(ctx)
+ ctx.Data["Page"] = pager
+
+ ctx.HTML(200, tplEmails)
+}
+
+var (
+ nullByte = []byte{0x00}
+)
+
+func isKeywordValid(keyword string) bool {
+ return !bytes.Contains([]byte(keyword), nullByte)
+}
+
+// ActivateEmail serves a POST request for activating/deactivating a user's email
+func ActivateEmail(ctx *context.Context) {
+
+ truefalse := map[string]bool{"1": true, "0": false}
+
+ uid := com.StrTo(ctx.Query("uid")).MustInt64()
+ email := ctx.Query("email")
+ primary, okp := truefalse[ctx.Query("primary")]
+ activate, oka := truefalse[ctx.Query("activate")]
+
+ if uid == 0 || len(email) == 0 || !okp || !oka {
+ ctx.Error(400)
+ return
+ }
+
+ log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
+
+ if err := models.ActivateUserEmail(uid, email, primary, activate); err != nil {
+ log.Error("ActivateUserEmail(%v,%v,%v,%v): %v", uid, email, primary, activate, err)
+ if models.IsErrEmailAlreadyUsed(err) {
+ ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
+ } else {
+ ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
+ }
+ } else {
+ log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
+ ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
+ }
+
+ redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")
+ q := url.Values{}
+ if val := ctx.QueryTrim("q"); len(val) > 0 {
+ q.Set("q", val)
+ }
+ if val := ctx.QueryTrim("sort"); len(val) > 0 {
+ q.Set("sort", val)
+ }
+ if val := ctx.QueryTrim("is_primary"); len(val) > 0 {
+ q.Set("is_primary", val)
+ }
+ if val := ctx.QueryTrim("is_activated"); len(val) > 0 {
+ q.Set("is_activated", val)
+ }
+ redirect.RawQuery = q.Encode()
+ ctx.Redirect(redirect.String())
+}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 9859ebc539..a8a08c9eca 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -444,6 +444,11 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/:userid/delete", admin.DeleteUser)
})
+ m.Group("/emails", func() {
+ m.Get("", admin.Emails)
+ m.Post("/activate", admin.ActivateEmail)
+ })
+
m.Group("/orgs", func() {
m.Get("", admin.Organizations)
})
diff --git a/routers/user/auth.go b/routers/user/auth.go
index 3a3e3a1a54..6d762a058c 100644
--- a/routers/user/auth.go
+++ b/routers/user/auth.go
@@ -1217,8 +1217,18 @@ func ActivateEmail(ctx *context.Context) {
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
+
+ if u, err := models.GetUserByID(email.UID); err != nil {
+ log.Warn("GetUserByID: %d", email.UID)
+ } else {
+ // Allow user to validate more emails
+ _ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
+ }
}
+ // FIXME: e-mail verification does not require the user to be logged in,
+ // so this could be redirecting to the login page.
+ // Should users be logged in automatically here? (consider 2FA requirements, etc.)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go
index a9064b0e15..3c0c64ca27 100644
--- a/routers/user/setting/account.go
+++ b/routers/user/setting/account.go
@@ -88,6 +88,51 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
+ // Send activation Email
+ if ctx.Query("_method") == "SENDACTIVATION" {
+ var address string
+ if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
+ log.Error("Send activation: activation still pending")
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ return
+ }
+ if ctx.Query("id") == "PRIMARY" {
+ if ctx.User.IsActive {
+ log.Error("Send activation: email not set for activation")
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ return
+ }
+ mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
+ address = ctx.User.Email
+ } else {
+ id := ctx.QueryInt64("id")
+ email, err := models.GetEmailAddressByID(ctx.User.ID, id)
+ if err != nil {
+ log.Error("GetEmailAddressByID(%d,%d) error: %v", ctx.User.ID, id, err)
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ return
+ }
+ if email == nil {
+ log.Error("Send activation: EmailAddress not found; user:%d, id: %d", ctx.User.ID, id)
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ return
+ }
+ if email.IsActivated {
+ log.Error("Send activation: email not set for activation")
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ return
+ }
+ mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
+ address = email.Email
+ }
+
+ if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
+ ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ return
+ }
// Set Email Notification Preference
if ctx.Query("_method") == "NOTIFICATION" {
preference := ctx.Query("preference")
@@ -134,7 +179,6 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
-
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
@@ -223,11 +267,25 @@ func UpdateUIThemePost(ctx *context.Context, form auth.UpdateThemeForm) {
}
func loadAccountData(ctx *context.Context) {
- emails, err := models.GetEmailAddresses(ctx.User.ID)
+ emlist, err := models.GetEmailAddresses(ctx.User.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
+ type UserEmail struct {
+ models.EmailAddress
+ CanBePrimary bool
+ }
+ pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName)
+ emails := make([]*UserEmail, len(emlist))
+ for i, em := range emlist {
+ var email UserEmail
+ email.EmailAddress = *em
+ email.CanBePrimary = em.IsActivated
+ emails[i] = &email
+ }
ctx.Data["Emails"] = emails
ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()
+ ctx.Data["ActivationsPending"] = pendingActivation
+ ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
}