1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

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>
This commit is contained in:
guillep2k
2020-03-02 15:25:36 -03:00
committed by GitHub
parent b5ecc82d6e
commit 5e1438ba92
12 changed files with 726 additions and 24 deletions

157
routers/admin/emails.go Normal file
View File

@@ -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())
}

View File

@@ -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)
})

View File

@@ -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")
}

View File

@@ -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
}