[GITEA] notifies admins on new user registration

Sends email with information on the new user (time of creation and time of last sign-in) and a link to manage the new user from the admin panel

closes: https://codeberg.org/forgejo/forgejo/issues/480

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/1371
Co-authored-by: Aravinth Manivannan <realaravinth@batsense.net>
Co-committed-by: Aravinth Manivannan <realaravinth@batsense.net>
(cherry picked from commit c721aa828b)
(cherry picked from commit 6487efcb9d)

Conflicts:
	modules/notification/base/notifier.go
	modules/notification/base/null.go
	modules/notification/notification.go
	https://codeberg.org/forgejo/forgejo/pulls/1422
(cherry picked from commit 7ea66ee1c5)

Conflicts:
	services/notify/notifier.go
	services/notify/notify.go
	services/notify/null.go
	https://codeberg.org/forgejo/forgejo/pulls/1469
(cherry picked from commit 7d2d997011)
(cherry picked from commit 435a54f140)
This commit is contained in:
Aravinth Manivannan 2023-09-07 07:11:29 +00:00 committed by Earl Warren
parent 476bd3c491
commit 8ec7b3e448
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
11 changed files with 218 additions and 1 deletions

View file

@ -1455,6 +1455,8 @@ LEVEL = Info
;; ;;
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
;DEFAULT_EMAIL_NOTIFICATIONS = enabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled
;; Send email notifications to all instance admins on new user sign-ups. Options: enabled, true, false
;NOTIFY_NEW_SIGN_UPS = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -7,6 +7,7 @@ package setting
var Admin struct { var Admin struct {
DisableRegularOrgCreation bool DisableRegularOrgCreation bool
DefaultEmailNotification string DefaultEmailNotification string
NotifyNewSignUps bool
} }
func loadAdminFrom(rootCfg ConfigProvider) { func loadAdminFrom(rootCfg ConfigProvider) {

View file

@ -441,6 +441,10 @@ activate_email = Verify your email address
activate_email.title = %s, please verify your email address activate_email.title = %s, please verify your email address
activate_email.text = Please click the following link to verify your email address within <b>%s</b>: activate_email.text = Please click the following link to verify your email address within <b>%s</b>:
admin.new_user.subject = New user %s
admin.new_user.user_info = User Information
admin.new_user.text = Please <a href="%s">click here</a> to manage the user from the admin panel.
register_notify = Welcome to Gitea register_notify = Welcome to Gitea
register_notify.title = %[1]s, welcome to %[2]s register_notify.title = %[1]s, welcome to %[2]s
register_notify.text_1 = this is your registration confirmation email for %s! register_notify.text_1 = this is your registration confirmation email for %s!

View file

@ -30,6 +30,7 @@ import (
"code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer" "code.gitea.io/gitea/services/mailer"
notify_service "code.gitea.io/gitea/services/notify"
"github.com/markbates/goth" "github.com/markbates/goth"
) )
@ -586,6 +587,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
} }
} }
notify_service.NewUserSignUp(ctx, u)
// update external user information // update external user information
if gothUser != nil { if gothUser != nil {
if err := externalaccount.UpdateExternalUser(u, *gothUser); err != nil { if err := externalaccount.UpdateExternalUser(u, *gothUser); err != nil {
@ -609,7 +611,6 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
ctx.Data["Email"] = u.Email ctx.Data["Email"] = u.Email
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale) ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
ctx.HTML(http.StatusOK, TplActivate) ctx.HTML(http.StatusOK, TplActivate)
if setting.CacheService.Enabled { if setting.CacheService.Enabled {
if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err) log.Error("Set cache(MailResendLimit) fail: %v", err)

View file

@ -0,0 +1,82 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package mailer
import (
"bytes"
"context"
"strconv"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
)
const (
tplNewUserMail base.TplName = "admin_new_user"
)
var sa = SendAsyncs
// MailNewUser sends notification emails on new user registrations to all admins
func MailNewUser(ctx context.Context, u *user_model.User) {
if !setting.Admin.NotifyNewSignUps {
return
}
if setting.MailService == nil {
// No mail service configured
return
}
recipients, err := user_model.GetAllUsers(ctx)
if err != nil {
log.Error("user_model.GetAllUsers: %v", err)
return
}
langMap := make(map[string][]string)
for _, r := range recipients {
if r.IsAdmin {
langMap[r.Language] = append(langMap[r.Language], r.Email)
}
}
for lang, tos := range langMap {
mailNewUser(ctx, u, lang, tos)
}
}
func mailNewUser(ctx context.Context, u *user_model.User, lang string, tos []string) {
locale := translation.NewLocale(lang)
subject := locale.Tr("mail.admin.new_user.subject", u.Name)
manageUserURL := setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10)
body := locale.Tr("mail.admin.new_user.text", manageUserURL)
mailMeta := map[string]any{
"NewUser": u,
"Subject": subject,
"Body": body,
"Language": locale.Language(),
"locale": locale,
"Str2html": templates.Str2html,
}
var mailBody bytes.Buffer
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewUserMail), mailMeta); err != nil {
log.Error("ExecuteTemplate [%s]: %v", string(tplNewUserMail)+"/body", err)
return
}
msgs := make([]*Message, 0, len(tos))
for _, to := range tos {
msg := NewMessage(to, subject, mailBody.String())
msg.Info = subject
msgs = append(msgs, msg)
}
sa(msgs)
}

View file

@ -0,0 +1,88 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package mailer
import (
"context"
"strconv"
"strings"
"testing"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func getTestUsers() []*user_model.User {
admin := new(user_model.User)
admin.Name = "admin"
admin.IsAdmin = true
admin.Language = "en_US"
admin.Email = "admin@forgejo.org"
newUser := new(user_model.User)
newUser.Name = "new_user"
newUser.Language = "en_US"
newUser.IsAdmin = false
newUser.Email = "new_user@forgejo.org"
newUser.LastLoginUnix = 1693648327
newUser.CreatedUnix = 1693648027
user_model.CreateUser(db.DefaultContext, admin)
user_model.CreateUser(db.DefaultContext, newUser)
users := make([]*user_model.User, 0)
users = append(users, admin)
users = append(users, newUser)
return users
}
func cleanUpUsers(ctx context.Context, users []*user_model.User) {
for _, u := range users {
db.DeleteByID(ctx, u.ID, new(user_model.User))
}
}
func TestAdminNotificationMail_test(t *testing.T) {
mailService := setting.Mailer{
From: "test@forgejo.org",
Protocol: "dummy",
}
setting.MailService = &mailService
setting.Domain = "localhost"
setting.AppSubURL = "http://localhost"
// test with NOTIFY_NEW_SIGNUPS enabled
setting.Admin.NotifyNewSignUps = true
ctx := context.Background()
NewContext(ctx)
users := getTestUsers()
oldSendAsyncs := sa
defer func() {
sa = oldSendAsyncs
cleanUpUsers(ctx, users)
}()
sa = func(msgs []*Message) {
assert.Equal(t, len(msgs), 1, "Test provides only one admin user, so only one email must be sent")
assert.Equal(t, msgs[0].To, users[0].Email, "checks if the recipient is the admin of the instance")
manageUserURL := "/admin/users/" + strconv.FormatInt(users[1].ID, 10)
assert.True(t, strings.ContainsAny(msgs[0].Body, manageUserURL), "checks if the message contains the link to manage the newly created user from the admin panel")
}
MailNewUser(ctx, users[1])
// test with NOTIFY_NEW_SIGNUPS disabled; emails shouldn't be sent
setting.Admin.NotifyNewSignUps = false
sa = func(msgs []*Message) {
assert.Equal(t, 1, 0, "this shouldn't execute. MailNewUser must exit early since NOTIFY_NEW_SIGNUPS is disabled")
}
MailNewUser(ctx, users[1])
}

View file

@ -202,3 +202,7 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *
log.Error("SendRepoTransferNotifyMail: %v", err) log.Error("SendRepoTransferNotifyMail: %v", err)
} }
} }
func (m *mailNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) {
MailNewUser(ctx, newUser)
}

View file

@ -59,6 +59,8 @@ type Notifier interface {
EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string)
DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page string) DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page string)
NewUserSignUp(ctx context.Context, newUser *user_model.User)
NewRelease(ctx context.Context, rel *repo_model.Release) NewRelease(ctx context.Context, rel *repo_model.Release)
UpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) UpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release)
DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release)

View file

@ -347,6 +347,13 @@ func RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, r
} }
} }
// NewUserSignUp notifies deletion of a package to notifiers
func NewUserSignUp(ctx context.Context, newUser *user_model.User) {
for _, notifier := range notifiers {
notifier.NewUserSignUp(ctx, newUser)
}
}
// PackageCreate notifies creation of a package to notifiers // PackageCreate notifies creation of a package to notifiers
func PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { func PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) {
for _, notifier := range notifiers { for _, notifier := range notifiers {

View file

@ -197,6 +197,10 @@ func (*NullNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, r
func (*NullNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) { func (*NullNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) {
} }
// NotifyNewUserSignUp notifies deletion of a package to notifiers
func (*NullNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) {
}
// PackageCreate places a place holder function // PackageCreate places a place holder function
func (*NullNotifier) PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { func (*NullNotifier) PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) {
} }

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>{{.Subject}}</title>
<style>
blockquote { padding-left: 1em; margin: 1em 0; border-left: 1px solid grey; color: #777}
.footer { font-size:small; color:#666;}
</style>
</head>
<body>
<ul>
<h3>{{.locale.Tr "mail.admin.new_user.user_info"}}</h3>
<li>{{.locale.Tr "admin.users.created"}}: {{DateTime "full" .NewUser.LastLoginUnix}}</li>
<li>{{.locale.Tr "admin.users.last_login"}}: {{DateTime "full" .NewUser.CreatedUnix}}</li>
</ul>
<p> {{.Body | Str2html}} </p>
</body>
</html>