mirror of https://github.com/go-gitea/gitea.git
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>pull/10580/head
parent
b5ecc82d6e
commit
5e1438ba92
@ -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())
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
{{template "base/head" .}}
|
||||||
|
<div class="admin user">
|
||||||
|
{{template "admin/navbar" .}}
|
||||||
|
<div class="ui container">
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{.i18n.Tr "admin.emails.email_manage_panel"}} ({{.i18n.Tr "admin.total" .Total}})
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<div class="ui right floated secondary filter menu">
|
||||||
|
<!-- Sort -->
|
||||||
|
<div class="ui dropdown type jump item">
|
||||||
|
<span class="text">
|
||||||
|
{{.i18n.Tr "repo.issues.filter_sort"}}
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
</span>
|
||||||
|
<div class="menu">
|
||||||
|
<a class="{{if or (eq .SortType "email") (not .SortType)}}active{{end}} item" href="{{$.Link}}?sort=email&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.email"}}</a>
|
||||||
|
<a class="{{if eq .SortType "reverseemail"}}active{{end}} item" href="{{$.Link}}?sort=reverseemail&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.email_reverse"}}</a>
|
||||||
|
<a class="{{if eq .SortType "username"}}active{{end}} item" href="{{$.Link}}?sort=username&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.name"}}</a>
|
||||||
|
<a class="{{if eq .SortType "reverseusername"}}active{{end}} item" href="{{$.Link}}?sort=reverseusername&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.name_reverse"}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form class="ui form ignore-dirty" style="max-width: 90%">
|
||||||
|
<div class="ui fluid action input">
|
||||||
|
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus>
|
||||||
|
<button class="ui blue button">{{.i18n.Tr "explore.search"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="ui attached table segment">
|
||||||
|
<table class="ui very basic striped table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{.i18n.Tr "admin.users.name"}}</th>
|
||||||
|
<th>{{.i18n.Tr "admin.users.full_name"}}</th>
|
||||||
|
<th>{{.i18n.Tr "email"}}</th>
|
||||||
|
<th>{{.i18n.Tr "admin.emails.primary"}}</th>
|
||||||
|
<th>{{.i18n.Tr "admin.emails.activated"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Emails}}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{AppSubUrl}}/{{.Name}}">{{.Name}}</a></td>
|
||||||
|
<td><span class="text truncate">{{.FullName}}</span></td>
|
||||||
|
<td><span class="text email">{{.Email}}</span></td>
|
||||||
|
<td><i class="fa fa{{if .IsPrimary}}-check{{end}}-square-o"></i></td>
|
||||||
|
<td>
|
||||||
|
{{if .CanChange}}
|
||||||
|
<a class="link-email-action" href data-uid="{{.UID}}"
|
||||||
|
data-email="{{.Email}}"
|
||||||
|
data-primary="{{if .IsPrimary}}1{{else}}0{{end}}"
|
||||||
|
data-activate="{{if .IsActivated}}0{{else}}1{{end}}">
|
||||||
|
<i class="fa fa{{if .IsActivated}}-check{{end}}-square-o"></i>
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
<i class="fa fa{{if .IsActivated}}-check{{end}}-square-o"></i>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "base/paginate" .}}
|
||||||
|
|
||||||
|
<div class="ui basic modal" id="change-email-modal">
|
||||||
|
<div class="ui icon header">
|
||||||
|
{{.i18n.Tr "admin.emails.change_email_header"}}
|
||||||
|
</div>
|
||||||
|
<div class="content center">
|
||||||
|
<p>{{.i18n.Tr "admin.emails.change_email_text"}}</p>
|
||||||
|
|
||||||
|
<form class="ui form" id="email-action-form" action="{{AppSubUrl}}/admin/emails/activate" method="post">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
|
||||||
|
<input type="hidden" id="query-sort" name="sort" value="{{.SortType}}">
|
||||||
|
<input type="hidden" id="query-keyword" name="q" value="{{.Keyword}}">
|
||||||
|
<input type="hidden" id="query-primary" name="is_primary" value="{{.IsPrimary}}" required>
|
||||||
|
<input type="hidden" id="query-activated" name="is_activated" value="{{.IsActivated}}" required>
|
||||||
|
|
||||||
|
<input type="hidden" id="form-uid" name="uid" value="" required>
|
||||||
|
<input type="hidden" id="form-email" name="email" value="" required>
|
||||||
|
<input type="hidden" id="form-primary" name="primary" value="" required>
|
||||||
|
<input type="hidden" id="form-activate" name="activate" value="" required>
|
||||||
|
|
||||||
|
<div class="center actions">
|
||||||
|
<div class="ui basic cancel inverted button">{{$.i18n.Tr "settings.cancel"}}</div>
|
||||||
|
<button class="ui basic inverted yellow button">{{$.i18n.Tr "modal.yes"}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/footer" .}}
|
Loading…
Reference in New Issue