diff options
author | guillep2k <18600385+guillep2k@users.noreply.github.com> | 2020-03-02 15:25:36 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-02 15:25:36 -0300 |
commit | 5e1438ba92fe5b4398ebf468e4ede21c7ef60409 (patch) | |
tree | 40096ee69a4ac5df46428a3cf1ae7ee3bee8078e /routers | |
parent | b5ecc82d6e22b5701bfadc1ebc430b9c7fef0cc8 (diff) | |
download | gitea-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.go | 157 | ||||
-rw-r--r-- | routers/routes/routes.go | 5 | ||||
-rw-r--r-- | routers/user/auth.go | 10 | ||||
-rw-r--r-- | routers/user/setting/account.go | 62 |
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 } |