From f1288d6d9bfc9150596cb2f7ddb7300cf7ab6952 Mon Sep 17 00:00:00 2001
From: Aravinth Manivannan <realaravinth@batsense.net>
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 <realaravinth@batsense.net>
Co-committed-by: Aravinth Manivannan <realaravinth@batsense.net>
(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 <dev.lh@web.de>
(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 e5a002a5cf..cc6b483615 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1456,6 +1456,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 16f8caf22f..b10e9b1b5b 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 60aa6b9a6f..95786a5a63 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 6104d427a2..543d8f1219 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -442,6 +442,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 <b>%s</b>:
 
+admin.new_user.subject = New user %s just signed up
+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.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 e27307ef1a..8553aaba6b 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"
 )
@@ -602,6 +603,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(ctx, u, *gothUser); err != nil {
@@ -625,7 +627,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 cc4e6baf0b..5c6e635b7a 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 @@
+<!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>