Browse Source
Admin page for managing user e-mail activation (#10557)
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>mj
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 724 additions and 22 deletions
-
281models/user_mail.go
-
66models/user_mail_test.go
-
19options/locale/locale_en-US.ini
-
157routers/admin/emails.go
-
5routers/routes/routes.go
-
10routers/user/auth.go
-
62routers/user/setting/account.go
-
101templates/admin/emails/list.tmpl
-
1templates/admin/nav.tmpl
-
3templates/admin/navbar.tmpl
-
29templates/user/settings/account.tmpl
-
12web_src/js/index.js
@ -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" .}} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue