From 6c7f16346f5e9dc5beadf950696b8db9f51b6eb1 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 7 Sep 2023 07:11:29 +0000 Subject: [PATCH] [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 Co-committed-by: Aravinth Manivannan (cherry picked from commit c721aa828ba6aec5ef95459cfc632a0a1f7463e9) (cherry picked from commit 6487efcb9da61be1f802f1cd8007330153322770) 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 7ea66ee1c5dd21d9e6a43f961e8adc71ec79b806) Conflicts: services/notify/notifier.go services/notify/notify.go services/notify/null.go https://codeberg.org/forgejo/forgejo/pulls/1469 (cherry picked from commit 7d2d9970115c94954dacb45684f9e3c16117ebfe) (cherry picked from commit 435a54f14039408b315c99063bdce28c7ef6fe2f) (cherry picked from commit 8ec7b3e4484383445fa2622a28bb4f5c990dd4f2) [GITEA] notifies admins on new user registration (squash) performance bottleneck Refs: https://codeberg.org/forgejo/forgejo/issues/1479 (cherry picked from commit 97ac9147ff3643cca0a059688c6b3c53479e28a7) (cherry picked from commit 19f295c16bd392aa438477fa3c42038d63d1a06a) (cherry picked from commit 3367dcb2cf5328e2afc89f7d5a008b64ede1c987) [GITEA] notifies admins on new user registration (squash) cosmetic changes Co-authored-by: delvh (cherry picked from commit 9f1670e040b469ed4346aa2689a75088e4e71c8b) (cherry picked from commit de5bb2a224ab2ae9be891de1ee88a7454a07f7e9) (cherry picked from commit 8f8e52f31a4da080465521747a2c5c0c51ed65e3) (cherry picked from commit e0d51303129fe8763d87ed5f859eeae8f0cc6188) --- custom/conf/app.example.ini | 2 + .../config-cheat-sheet.en-us.md | 1 + models/user/user.go | 6 ++ models/user/user_test.go | 10 +++ modules/setting/admin.go | 5 +- options/locale/locale_en-US.ini | 4 + routers/web/auth/auth.go | 3 +- services/mailer/mail_admin_new_user.go | 80 +++++++++++++++++ services/mailer/mail_admin_new_user_test.go | 88 +++++++++++++++++++ services/mailer/notify.go | 4 + services/notify/notifier.go | 2 + services/notify/notify.go | 7 ++ services/notify/null.go | 3 + templates/mail/notify/admin_new_user.tmpl | 22 +++++ 14 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 services/mailer/mail_admin_new_user.go create mode 100644 services/mailer/mail_admin_new_user_test.go create mode 100644 templates/mail/notify/admin_new_user.tmpl diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 90f0fccf74..c834365d9d 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1455,6 +1455,8 @@ LEVEL = Info ;; ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled +;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false +;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index a337ae82a0..13a791ef4f 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -510,6 +510,7 @@ And the following unique queues: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations. +- `SEND_NOTIFICATION_EMAIL_ON_NEW_USER`: **false**: Send an email to all admins when a new user signs up to inform the admins about this act. ## Security (`security`) diff --git a/models/user/user.go b/models/user/user.go index 63b95816ce..a3bdff33d2 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -223,6 +223,12 @@ func GetAllUsers(ctx context.Context) ([]*User, error) { return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users) } +// GetAllAdmins returns a slice of all adminusers found in DB. +func GetAllAdmins(ctx context.Context) ([]*User, error) { + users := make([]*User, 0) + return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).And("is_admin = ?", true).Find(&users) +} + // IsLocal returns true if user login type is LoginPlain. func (u *User) IsLocal() bool { return u.LoginType <= auth.Plain diff --git a/models/user/user_test.go b/models/user/user_test.go index 971117482c..fed2cb2c48 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -544,3 +544,13 @@ func Test_ValidateUser(t *testing.T) { assert.EqualValues(t, expected, err == nil, fmt.Sprintf("case: %+v", kase)) } } + +func TestGetAllAdmins(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + admins, err := user_model.GetAllAdmins(db.DefaultContext) + assert.NoError(t, err) + + assert.Len(t, admins, 1) + assert.Equal(t, int64(1), admins[0].ID) +} diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 2d2dd26de9..d7f0ee827d 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -5,8 +5,9 @@ package setting // Admin settings var Admin struct { - DisableRegularOrgCreation bool - DefaultEmailNotification string + DisableRegularOrgCreation bool + DefaultEmailNotification string + SendNotificationEmailOnNewUser bool } func loadAdminFrom(rootCfg ConfigProvider) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 15b0e4ef59..94e5f2f9c8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -440,6 +440,10 @@ activate_email = 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 %s: +admin.new_user.subject = New user %s just signed up +admin.new_user.user_info = User Information +admin.new_user.text = Please click here to manage the user from the admin panel. + register_notify = Welcome to Gitea register_notify.title = %[1]s, welcome to %[2]s register_notify.text_1 = this is your registration confirmation email for %s! diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 8017602d99..465077a909 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/mailer" + notify_service "code.gitea.io/gitea/services/notify" "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 if gothUser != 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["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale) ctx.HTML(http.StatusOK, TplActivate) - if setting.CacheService.Enabled { if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { log.Error("Set cache(MailResendLimit) fail: %v", err) diff --git a/services/mailer/mail_admin_new_user.go b/services/mailer/mail_admin_new_user.go new file mode 100644 index 0000000000..47b110412c --- /dev/null +++ b/services/mailer/mail_admin_new_user.go @@ -0,0 +1,80 @@ +// 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 = "notify/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.SendNotificationEmailOnNewUser { + return + } + + if setting.MailService == nil { + // No mail service configured + return + } + + recipients, err := user_model.GetAllAdmins(ctx) + if err != nil { + log.Error("user_model.GetAllAdmins: %v", err) + return + } + + langMap := make(map[string][]string) + for _, r := range recipients { + 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) +} diff --git a/services/mailer/mail_admin_new_user_test.go b/services/mailer/mail_admin_new_user_test.go new file mode 100644 index 0000000000..4bb8d6a7a0 --- /dev/null +++ b/services/mailer/mail_admin_new_user_test.go @@ -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@example.com" + + newUser := new(user_model.User) + newUser.Name = "new_user" + newUser.Language = "en_US" + newUser.IsAdmin = false + newUser.Email = "new_user@example.com" + 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@example.com", + Protocol: "dummy", + } + + setting.MailService = &mailService + setting.Domain = "localhost" + setting.AppSubURL = "http://localhost" + + // test with SEND_NOTIFICATION_EMAIL_ON_NEW_USER enabled + setting.Admin.SendNotificationEmailOnNewUser = 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 SEND_NOTIFICATION_EMAIL_ON_NEW_USER disabled; emails shouldn't be sent + setting.Admin.SendNotificationEmailOnNewUser = false + sa = func(msgs []*Message) { + assert.Equal(t, 1, 0, "this shouldn't execute. MailNewUser must exit early since SEND_NOTIFICATION_EMAIL_ON_NEW_USER is disabled") + } + + MailNewUser(ctx, users[1]) +} diff --git a/services/mailer/notify.go b/services/mailer/notify.go index 9eaf268d0a..1577e824a5 100644 --- a/services/mailer/notify.go +++ b/services/mailer/notify.go @@ -202,3 +202,7 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner * log.Error("SendRepoTransferNotifyMail: %v", err) } } + +func (m *mailNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) { + MailNewUser(ctx, newUser) +} diff --git a/services/notify/notifier.go b/services/notify/notifier.go index ed053a812a..3230a5e5f5 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -59,6 +59,8 @@ type Notifier interface { 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) + NewUserSignUp(ctx context.Context, newUser *user_model.User) + NewRelease(ctx context.Context, 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) diff --git a/services/notify/notify.go b/services/notify/notify.go index 16fbb6325d..9cb329d302 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -347,6 +347,13 @@ func RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, r } } +// NewUserSignUp notifies about a newly signed up user 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 func PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { for _, notifier := range notifiers { diff --git a/services/notify/null.go b/services/notify/null.go index dddd421bef..894d118eac 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -197,6 +197,9 @@ 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) NewUserSignUp(ctx context.Context, newUser *user_model.User) { +} + // PackageCreate places a place holder function func (*NullNotifier) PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { } diff --git a/templates/mail/notify/admin_new_user.tmpl b/templates/mail/notify/admin_new_user.tmpl new file mode 100644 index 0000000000..58b5c264e7 --- /dev/null +++ b/templates/mail/notify/admin_new_user.tmpl @@ -0,0 +1,22 @@ + + + + + {{.Subject}} + + + + + + +
    +

    {{.locale.Tr "mail.admin.new_user.user_info"}}

    +
  • {{.locale.Tr "admin.users.created"}}: {{DateTime "full" .NewUser.LastLoginUnix}}
  • +
  • {{.locale.Tr "admin.users.last_login"}}: {{DateTime "full" .NewUser.CreatedUnix}}
  • +
+

{{.Body | Str2html}}

+ +