Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
1 change: 1 addition & 0 deletions docs/content/administration/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
6 changes: 6 additions & 0 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions models/user/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
5 changes: 3 additions & 2 deletions modules/setting/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,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!
Expand Down
3 changes: 2 additions & 1 deletion routers/web/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
81 changes: 81 additions & 0 deletions services/mailer/mail_admin_new_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The Forgejo Authors. All rights reserved.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I summon @techknowlogick

// 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)
}
89 changes: 89 additions & 0 deletions services/mailer/mail_admin_new_user_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The Forgejo 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])
}
4 changes: 4 additions & 0 deletions services/mailer/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions services/notify/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions services/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions services/notify/null.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}
Expand Down
22 changes: 22 additions & 0 deletions templates/mail/notify/admin_new_user.tmpl
Original file line number Diff line number Diff line change
@@ -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>