[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)
(cherry picked from commit 8ec7b3e448)

[GITEA] notifies admins on new user registration (squash) performance bottleneck

Refs: https://codeberg.org/forgejo/forgejo/issues/1479
(cherry picked from commit 97ac9147ff)
(cherry picked from commit 19f295c16b)
(cherry picked from commit 3367dcb2cf)

[GITEA] notifies admins on new user registration (squash) cosmetic changes

Co-authored-by: delvh <dev.lh@web.de>
(cherry picked from commit 9f1670e040)
(cherry picked from commit de5bb2a224)
(cherry picked from commit 8f8e52f31a)
(cherry picked from commit e0d5130312)
(cherry picked from commit f1288d6d9b)
(cherry picked from commit 1db4736fd7)
(cherry picked from commit e8dcbb6cd6)
(cherry picked from commit 09625d6476)

[GITEA] notifies admins on new user registration (squash) ctx.Locale

(cherry picked from commit dab7212fad)
(cherry picked from commit 9b7bbae8c4)
(cherry picked from commit f750b71d3d)
(cherry picked from commit f79af36679)
(cherry picked from commit e76eee334e)

[GITEA] notifies admins on new user registration (squash) fix locale

(cherry picked from commit 54cd100d8d)
(cherry picked from commit 053dbd3d50)

[GITEA] notifies admins on new user registration (squash) fix URL

1. Use absolute URL in the admin panel link sent on new registrations
2. Include absolute URL of the newly signed-up user's profile.

New email looks like this:

<details><summary>Please click to expand</summary>

```
--153937b1864f158f4fd145c4b5d4a513568681dd489021dd466a8ad7b770
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=UTF-8

User Information: @realaravinth ( http://localhost:3000/realaravinth )
----------------------------------------------------------------------

* Created: 2023-12-13 19:36:50 +05:30

Please click here ( http://localhost:3000/admin/users/9 ) to manage the use=
r from the admin panel.
--153937b1864f158f4fd145c4b5d4a513568681dd489021dd466a8ad7b770
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html>
<html>
<head>
	<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
	<title>New user realaravinth just signed up</title>

	<style>
		blockquote { padding-left: 1em; margin: 1em 0; border-left: 1px solid gre=
y; color: #777}
		.footer { font-size:small; color:#666;}
	</style>

</head>

<body>
	<ul>
		<h3>User Information: <a href=3D"http://localhost:3000/realaravinth">@rea=
laravinth</a></h3>
		<li>Created: <relative-time format=3D"datetime" weekday=3D"" year=3D"nume=
ric" month=3D"short" day=3D"numeric" hour=3D"numeric" minute=3D"numeric" se=
cond=3D"numeric" datetime=3D"2023-12-13T19:36:50+05:30">2023-12-13 19:36:50=
 +05:30</relative-time></li>
	</ul>
	<p> Please <a href=3D"http://localhost:3000/admin/users/9" rel=3D"nofollow=
">click here</a> to manage the user from the admin panel. </p>
</body>
</html>

--153937b1864f158f4fd145c4b5d4a513568681dd489021dd466a8ad7b770--
```

</details>

fixes: https://codeberg.org/forgejo/forgejo/issues/1927
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/1940
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Aravinth Manivannan <realaravinth@batsense.net>
Co-committed-by: Aravinth Manivannan <realaravinth@batsense.net>
(cherry picked from commit b8d764e36a)
(cherry picked from commit d48b84f623)

Conflicts:
	routers/web/auth/auth.go
	https://codeberg.org/forgejo/forgejo/pulls/2034
(cherry picked from commit 02d3c125cc)
(cherry picked from commit 367374ecc3)

Conflicts:
	models/user/user_test.go
	https://codeberg.org/forgejo/forgejo/pulls/2119
(cherry picked from commit 4124fa5aa4)
This commit is contained in:
Aravinth Manivannan 2023-09-07 07:11:29 +00:00 committed by Earl Warren
parent eb1378b843
commit 7f12610ff6
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
14 changed files with 244 additions and 2 deletions

View file

@ -1476,6 +1476,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 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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -517,6 +517,7 @@ And the following unique queues:
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `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. - `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`) ## Security (`security`)

View file

@ -228,6 +228,12 @@ func GetAllUsers(ctx context.Context) ([]*User, error) {
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users) 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. // IsLocal returns true if user login type is LoginPlain.
func (u *User) IsLocal() bool { func (u *User) IsLocal() bool {
return u.LoginType <= auth.Plain return u.LoginType <= auth.Plain

View file

@ -527,6 +527,16 @@ func TestIsUserVisibleToViewer(t *testing.T) {
test(user31, nil, false) test(user31, nil, false)
} }
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)
}
func Test_ValidateUser(t *testing.T) { func Test_ValidateUser(t *testing.T) {
oldSetting := setting.Service.AllowedUserVisibilityModesSlice oldSetting := setting.Service.AllowedUserVisibilityModesSlice
defer func() { defer func() {

View file

@ -5,8 +5,9 @@ package setting
// Admin settings // Admin settings
var Admin struct { var Admin struct {
DisableRegularOrgCreation bool DisableRegularOrgCreation bool
DefaultEmailNotification string DefaultEmailNotification string
SendNotificationEmailOnNewUser 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 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 = 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

@ -32,6 +32,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"
) )
@ -600,6 +601,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(ctx, u, *gothUser); err != nil { if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {

View file

@ -0,0 +1,81 @@
// 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 = SendAsync
// 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)
manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(u.ID, 10)
subject := locale.Tr("mail.admin.new_user.subject", u.Name)
body := locale.Tr("mail.admin.new_user.text", manageUserURL)
mailMeta := map[string]any{
"NewUser": u,
"NewUserUrl": u.HTMLURL(),
"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,98 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package mailer
import (
"context"
"strconv"
"testing"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func getTestUsers(t *testing.T) []*user_model.User {
t.Helper()
admin := new(user_model.User)
admin.Name = "testadmin"
admin.IsAdmin = true
admin.Language = "en_US"
admin.Email = "admin@example.com"
require.NoError(t, user_model.CreateUser(db.DefaultContext, admin))
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
require.NoError(t, user_model.CreateUser(db.DefaultContext, newUser))
return []*user_model.User{admin, newUser}
}
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) {
translation.InitLocales(context.Background())
locale := translation.NewLocale("")
key := "mail.admin.new_user.user_info"
translatedKey := locale.Tr(key)
require.NotEqualValues(t, key, translatedKey)
mailService := setting.Mailer{
From: "test@example.com",
Protocol: "dummy",
}
setting.MailService = &mailService
// test with SEND_NOTIFICATION_EMAIL_ON_NEW_USER enabled
setting.Admin.SendNotificationEmailOnNewUser = true
ctx := context.Background()
NewContext(ctx)
users := getTestUsers(t)
oldSendAsync := sa
defer func() {
sa = oldSendAsync
cleanUpUsers(ctx, users)
}()
called := false
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 := setting.AppURL + "admin/users/" + strconv.FormatInt(users[1].ID, 10)
assert.Contains(t, msgs[0].Body, manageUserURL)
assert.Contains(t, msgs[0].Body, users[1].HTMLURL())
assert.Contains(t, msgs[0].Body, translatedKey, "the .Locale translates to nothing")
assert.Contains(t, msgs[0].Body, users[1].Name, "user name of the newly created user")
for _, untranslated := range []string{"mail.admin", "admin.users"} {
assert.NotContains(t, msgs[0].Body, untranslated, "this is an untranslated placeholder prefix")
}
called = true
}
MailNewUser(ctx, users[1])
assert.True(t, called)
// 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])
}

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 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 // 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,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) 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 // 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,21 @@
<!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" | Str2html}}: <a href="{{.NewUserUrl}}">@{{.NewUser.Name}}</a></h3>
<li>{{.Locale.Tr "admin.users.created" | Str2html}}: {{DateTime "full" .NewUser.CreatedUnix}}</li>
</ul>
<p> {{.Body | Str2html}} </p>
</body>
</html>