mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-27 16:35:57 +00:00
fix: only send Forgejo Actions notifications to one user (#8227)
- If the run was attributed to a system user (which is the case for scheduled runs for instance), ignore it and fallback to the mail of the owner. System users may have email addresses, but they are not to be used. - If the owner is a system user or an organization with no email associated with it, do nothing. - If a user with an email exists, check if they did not disable notifications and send the email. Refs: https://codeberg.org/forgejo/forgejo/issues/8187 Refs: https://codeberg.org/forgejo/forgejo/issues/8233 ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. ### Documentation - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] I do not want this change to show in the release notes. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8227 Reviewed-by: Christopher Besch <mail@chris-besch.com> Co-authored-by: Earl Warren <contact@earl-warren.org> Co-committed-by: Earl Warren <contact@earl-warren.org>
This commit is contained in:
parent
25d596d387
commit
9e6f722f94
5 changed files with 201 additions and 105 deletions
|
@ -12,6 +12,13 @@ import (
|
||||||
"forgejo.org/modules/structs"
|
"forgejo.org/modules/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IsSystem returns true if the user has a fixed
|
||||||
|
// negative ID, is never stored in the database and
|
||||||
|
// is generated on the fly when needed.
|
||||||
|
func (u *User) IsSystem() bool {
|
||||||
|
return u.IsGhost() || u.IsActions()
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GhostUserID = -1
|
GhostUserID = -1
|
||||||
GhostUserName = "Ghost"
|
GhostUserName = "Ghost"
|
||||||
|
|
|
@ -1308,7 +1308,7 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special user that can't have associated contributions and permissions in the repo.
|
// Special user that can't have associated contributions and permissions in the repo.
|
||||||
if poster.IsGhost() || poster.IsActions() || poster.IsAPServerActor() {
|
if poster.IsSystem() || poster.IsAPServerActor() {
|
||||||
return roleDescriptor, nil
|
return roleDescriptor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,19 +23,20 @@ func MailActionRun(run *actions_model.ActionRun, priorStatus actions_model.Statu
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if run.TriggerUser.Email != "" && run.TriggerUser.EmailNotificationsPreference != user_model.EmailNotificationsDisabled {
|
user := run.TriggerUser
|
||||||
if err := sendMailActionRun(run.TriggerUser, run, priorStatus, lastRun); err != nil {
|
// this happens e.g. when this is a scheduled run
|
||||||
return err
|
if user.IsSystem() {
|
||||||
}
|
user = run.Repo.Owner
|
||||||
|
}
|
||||||
|
if user.IsSystem() || user.Email == "" {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if run.Repo.Owner.Email != "" && run.Repo.Owner.Email != run.TriggerUser.Email && run.Repo.Owner.EmailNotificationsPreference != user_model.EmailNotificationsDisabled {
|
if user.EmailNotificationsPreference == user_model.EmailNotificationsDisabled {
|
||||||
if err := sendMailActionRun(run.Repo.Owner, run, priorStatus, lastRun); err != nil {
|
return nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return sendMailActionRun(user, run, priorStatus, lastRun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
|
func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
|
||||||
|
|
|
@ -4,42 +4,53 @@
|
||||||
package mailer
|
package mailer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
actions_model "forgejo.org/models/actions"
|
actions_model "forgejo.org/models/actions"
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
|
organization_model "forgejo.org/models/organization"
|
||||||
repo_model "forgejo.org/models/repo"
|
repo_model "forgejo.org/models/repo"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/optional"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
notify_service "forgejo.org/services/notify"
|
notify_service "forgejo.org/services/notify"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getActionsNowDoneTestUsers(t *testing.T) []*user_model.User {
|
func getActionsNowDoneTestUser(t *testing.T, name, email, notifications string) *user_model.User {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
newTriggerUser := new(user_model.User)
|
user := new(user_model.User)
|
||||||
newTriggerUser.Name = "new_trigger_user"
|
user.Name = name
|
||||||
newTriggerUser.Language = "en_US"
|
user.Language = "en_US"
|
||||||
newTriggerUser.IsAdmin = false
|
user.IsAdmin = false
|
||||||
newTriggerUser.Email = "new_trigger_user@example.com"
|
user.Email = email
|
||||||
newTriggerUser.LastLoginUnix = 1693648327
|
user.LastLoginUnix = 1693648327
|
||||||
newTriggerUser.CreatedUnix = 1693648027
|
user.CreatedUnix = 1693648027
|
||||||
newTriggerUser.EmailNotificationsPreference = user_model.EmailNotificationsEnabled
|
opts := user_model.CreateUserOverwriteOptions{
|
||||||
require.NoError(t, user_model.CreateUser(db.DefaultContext, newTriggerUser))
|
AllowCreateOrganization: optional.Some(true),
|
||||||
|
EmailNotificationsPreference: ¬ifications,
|
||||||
|
}
|
||||||
|
require.NoError(t, user_model.AdminCreateUser(db.DefaultContext, user, &opts))
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
newOwner := new(user_model.User)
|
func getActionsNowDoneTestOrg(t *testing.T, name, email string, owner *user_model.User) *user_model.User {
|
||||||
newOwner.Name = "new_owner"
|
t.Helper()
|
||||||
newOwner.Language = "en_US"
|
org := new(organization_model.Organization)
|
||||||
newOwner.IsAdmin = false
|
org.Name = name
|
||||||
newOwner.Email = "new_owner@example.com"
|
org.Language = "en_US"
|
||||||
newOwner.LastLoginUnix = 1693648329
|
org.IsAdmin = false
|
||||||
newOwner.CreatedUnix = 1693648029
|
// contact email for the organization, for display purposes but otherwise not used as of v12
|
||||||
newOwner.EmailNotificationsPreference = user_model.EmailNotificationsEnabled
|
org.Email = email
|
||||||
require.NoError(t, user_model.CreateUser(db.DefaultContext, newOwner))
|
org.LastLoginUnix = 1693648327
|
||||||
|
org.CreatedUnix = 1693648027
|
||||||
return []*user_model.User{newTriggerUser, newOwner}
|
org.Email = email
|
||||||
|
require.NoError(t, organization_model.CreateOrganization(db.DefaultContext, org, owner))
|
||||||
|
return (*user_model.User)(org)
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) {
|
func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) {
|
||||||
|
@ -49,98 +60,169 @@ func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) {
|
||||||
func TestActionRunNowDoneNotificationMail(t *testing.T) {
|
func TestActionRunNowDoneNotificationMail(t *testing.T) {
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|
||||||
users := getActionsNowDoneTestUsers(t)
|
defer test.MockVariableValue(&setting.Admin.DisableRegularOrgCreation, false)()
|
||||||
defer CleanUpUsers(ctx, users)
|
|
||||||
triggerUser := users[0]
|
actionsUser := user_model.NewActionsUser()
|
||||||
ownerUser := users[1]
|
require.NotEmpty(t, actionsUser.Email)
|
||||||
|
|
||||||
repo := repo_model.Repository{
|
repo := repo_model.Repository{
|
||||||
Name: "some repo",
|
Name: "some repo",
|
||||||
Description: "rockets are cool",
|
Description: "rockets are cool",
|
||||||
Owner: ownerUser,
|
|
||||||
OwnerID: ownerUser.ID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do some funky stuff with the action run's ids:
|
// Do some funky stuff with the action run's ids:
|
||||||
// The run with the larger ID finished first.
|
// The run with the larger ID finished first.
|
||||||
// This is odd but something that must work.
|
// This is odd but something that must work.
|
||||||
run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"}
|
run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"}
|
||||||
run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"}
|
run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"}
|
||||||
|
|
||||||
|
assignUsers := func(triggerUser, owner *user_model.User) {
|
||||||
|
for _, run := range []*actions_model.ActionRun{run1, run2} {
|
||||||
|
run.TriggerUser = triggerUser
|
||||||
|
run.TriggerUserID = triggerUser.ID
|
||||||
|
}
|
||||||
|
repo.Owner = owner
|
||||||
|
repo.OwnerID = owner.ID
|
||||||
|
}
|
||||||
|
|
||||||
notify_service.RegisterNotifier(NewNotifier())
|
notify_service.RegisterNotifier(NewNotifier())
|
||||||
|
|
||||||
|
orgOwner := getActionsNowDoneTestUser(t, "org_owner", "org_owner@example.com", "disabled")
|
||||||
|
defer CleanUpUsers(ctx, []*user_model.User{orgOwner})
|
||||||
|
|
||||||
t.Run("DontSendNotificationEmailOnFirstActionSuccess", func(t *testing.T) {
|
t.Run("DontSendNotificationEmailOnFirstActionSuccess", func(t *testing.T) {
|
||||||
|
user := getActionsNowDoneTestUser(t, "new_user", "new_user@example.com", "enabled")
|
||||||
|
defer CleanUpUsers(ctx, []*user_model.User{user})
|
||||||
|
assignUsers(user, user)
|
||||||
defer MockMailSettings(func(msgs ...*Message) {
|
defer MockMailSettings(func(msgs ...*Message) {
|
||||||
assert.Fail(t, "no mail should be sent")
|
assert.Fail(t, "no mail should be sent")
|
||||||
})()
|
})()
|
||||||
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
|
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) {
|
for _, testCase := range []struct {
|
||||||
mailSentToOwner := false
|
name string
|
||||||
mailSentToTriggerUser := false
|
triggerUser *user_model.User
|
||||||
defer MockMailSettings(func(msgs ...*Message) {
|
owner *user_model.User
|
||||||
assert.LessOrEqual(t, len(msgs), 2)
|
expected string
|
||||||
for _, msg := range msgs {
|
expectMail bool
|
||||||
switch msg.To {
|
}{
|
||||||
case triggerUser.EmailTo():
|
{
|
||||||
assert.False(t, mailSentToTriggerUser, "sent mail twice")
|
// if the action is assigned a trigger user in a repository
|
||||||
mailSentToTriggerUser = true
|
// owned by a regular user, the mail is sent to the trigger user
|
||||||
case ownerUser.EmailTo():
|
name: "RegularTriggerUser",
|
||||||
assert.False(t, mailSentToOwner, "sent mail twice")
|
triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user0", "new_trigger_user0@example.com", user_model.EmailNotificationsEnabled),
|
||||||
mailSentToOwner = true
|
owner: getActionsNowDoneTestUser(t, "new_owner0", "new_owner0@example.com", user_model.EmailNotificationsEnabled),
|
||||||
default:
|
expected: "trigger",
|
||||||
assert.Fail(t, "sent mail to unknown sender", msg.To)
|
expectMail: true,
|
||||||
}
|
},
|
||||||
assert.Contains(t, msg.Body, triggerUser.HTMLURL())
|
{
|
||||||
assert.Contains(t, msg.Body, triggerUser.Name)
|
// if the action is assigned to a system user (e.g. ActionsUser)
|
||||||
// what happened
|
// in a repository owned by a regular user, the mail is sent to
|
||||||
assert.Contains(t, msg.Body, "failed")
|
// the user that owns the repository
|
||||||
// new status of run
|
name: "SystemTriggerUserAndRegularOwner",
|
||||||
assert.Contains(t, msg.Body, "failure")
|
triggerUser: actionsUser,
|
||||||
// prior status of this run
|
owner: getActionsNowDoneTestUser(t, "new_owner1", "new_owner1@example.com", user_model.EmailNotificationsEnabled),
|
||||||
assert.Contains(t, msg.Body, "waiting")
|
expected: "owner",
|
||||||
assertTranslatedLocaleMailActionsNowDone(t, msg.Body)
|
expectMail: true,
|
||||||
}
|
},
|
||||||
})()
|
{
|
||||||
notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil)
|
// if the action is assigned a trigger user with disabled notifications in a repository
|
||||||
assert.True(t, mailSentToOwner)
|
// owned by a regular user, no mail is sent
|
||||||
assert.True(t, mailSentToTriggerUser)
|
name: "RegularTriggerUserNotificationsDisabled",
|
||||||
})
|
triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user2", "new_trigger_user2@example.com", user_model.EmailNotificationsDisabled),
|
||||||
|
owner: getActionsNowDoneTestUser(t, "new_owner2", "new_owner2@example.com", user_model.EmailNotificationsEnabled),
|
||||||
|
expectMail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// if the action is assigned to a system user (e.g. ActionsUser)
|
||||||
|
// owned by a regular user with disabled notifications, no mail is sent
|
||||||
|
name: "SystemTriggerUserAndRegularOwnerNotificationsDisabled",
|
||||||
|
triggerUser: actionsUser,
|
||||||
|
owner: getActionsNowDoneTestUser(t, "new_owner3", "new_owner3@example.com", user_model.EmailNotificationsDisabled),
|
||||||
|
expectMail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// if the action is assigned to a system user (e.g. ActionsUser)
|
||||||
|
// in a repository owned by an organization with an email contact, the mail is sent to
|
||||||
|
// this email contact
|
||||||
|
name: "SystemTriggerUserAndOrgOwner",
|
||||||
|
triggerUser: actionsUser,
|
||||||
|
owner: getActionsNowDoneTestOrg(t, "new_org1", "new_org_owner0@example.com", orgOwner),
|
||||||
|
expected: "owner",
|
||||||
|
expectMail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// if the action is assigned to a system user (e.g. ActionsUser)
|
||||||
|
// in a repository owned by an organization without an email contact, no mail is sent
|
||||||
|
name: "SystemTriggerUserAndNoMailOrgOwner",
|
||||||
|
triggerUser: actionsUser,
|
||||||
|
owner: getActionsNowDoneTestOrg(t, "new_org2", "", orgOwner),
|
||||||
|
expectMail: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
assignUsers(testCase.triggerUser, testCase.owner)
|
||||||
|
defer CleanUpUsers(ctx, slices.DeleteFunc([]*user_model.User{testCase.triggerUser, testCase.owner}, func(user *user_model.User) bool {
|
||||||
|
return user.IsSystem()
|
||||||
|
}))
|
||||||
|
|
||||||
t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) {
|
t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) {
|
||||||
mailSentToOwner := false
|
mailSent := false
|
||||||
mailSentToTriggerUser := false
|
defer MockMailSettings(func(msgs ...*Message) {
|
||||||
defer MockMailSettings(func(msgs ...*Message) {
|
assert.Len(t, msgs, 1)
|
||||||
assert.LessOrEqual(t, len(msgs), 2)
|
msg := msgs[0]
|
||||||
for _, msg := range msgs {
|
assert.False(t, mailSent, "sent mail twice")
|
||||||
switch msg.To {
|
expectedEmail := testCase.triggerUser.Email
|
||||||
case triggerUser.EmailTo():
|
if testCase.expected == "owner" { // otherwise "trigger"
|
||||||
assert.False(t, mailSentToTriggerUser, "sent mail twice")
|
expectedEmail = testCase.owner.Email
|
||||||
mailSentToTriggerUser = true
|
}
|
||||||
case ownerUser.EmailTo():
|
require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender")
|
||||||
assert.False(t, mailSentToOwner, "sent mail twice")
|
mailSent = true
|
||||||
mailSentToOwner = true
|
assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL())
|
||||||
default:
|
assert.Contains(t, msg.Body, testCase.triggerUser.Name)
|
||||||
assert.Fail(t, "sent mail to unknown sender", msg.To)
|
// what happened
|
||||||
}
|
assert.Contains(t, msg.Body, "failed")
|
||||||
assert.Contains(t, msg.Body, triggerUser.HTMLURL())
|
// new status of run
|
||||||
assert.Contains(t, msg.Body, triggerUser.Name)
|
assert.Contains(t, msg.Body, "failure")
|
||||||
// what happened
|
// prior status of this run
|
||||||
assert.Contains(t, msg.Body, "recovered")
|
assert.Contains(t, msg.Body, "waiting")
|
||||||
// old status of run
|
assertTranslatedLocaleMailActionsNowDone(t, msg.Body)
|
||||||
assert.Contains(t, msg.Body, "failure")
|
})()
|
||||||
// new status of run
|
require.NotNil(t, setting.MailService)
|
||||||
assert.Contains(t, msg.Body, "success")
|
|
||||||
// prior status of this run
|
|
||||||
assert.Contains(t, msg.Body, "running")
|
|
||||||
assertTranslatedLocaleMailActionsNowDone(t, msg.Body)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
assert.NotNil(t, setting.MailService)
|
|
||||||
|
|
||||||
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1)
|
notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil)
|
||||||
assert.True(t, mailSentToOwner)
|
assert.Equal(t, testCase.expectMail, mailSent)
|
||||||
assert.True(t, mailSentToTriggerUser)
|
})
|
||||||
})
|
|
||||||
|
t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) {
|
||||||
|
mailSent := false
|
||||||
|
defer MockMailSettings(func(msgs ...*Message) {
|
||||||
|
assert.Len(t, msgs, 1)
|
||||||
|
msg := msgs[0]
|
||||||
|
assert.False(t, mailSent, "sent mail twice")
|
||||||
|
expectedEmail := testCase.triggerUser.Email
|
||||||
|
if testCase.expected == "owner" { // otherwise "trigger"
|
||||||
|
expectedEmail = testCase.owner.Email
|
||||||
|
}
|
||||||
|
require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender")
|
||||||
|
mailSent = true
|
||||||
|
assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL())
|
||||||
|
assert.Contains(t, msg.Body, testCase.triggerUser.Name)
|
||||||
|
// what happened
|
||||||
|
assert.Contains(t, msg.Body, "recovered")
|
||||||
|
// old status of run
|
||||||
|
assert.Contains(t, msg.Body, "failure")
|
||||||
|
// new status of run
|
||||||
|
assert.Contains(t, msg.Body, "success")
|
||||||
|
// prior status of this run
|
||||||
|
assert.Contains(t, msg.Body, "running")
|
||||||
|
})()
|
||||||
|
require.NotNil(t, setting.MailService)
|
||||||
|
|
||||||
|
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1)
|
||||||
|
assert.Equal(t, testCase.expectMail, mailSent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
|
organization_model "forgejo.org/models/organization"
|
||||||
"forgejo.org/models/unittest"
|
"forgejo.org/models/unittest"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
|
@ -51,6 +52,11 @@ func MockMailSettings(send func(msgs ...*Message)) func() {
|
||||||
|
|
||||||
func CleanUpUsers(ctx context.Context, users []*user_model.User) {
|
func CleanUpUsers(ctx context.Context, users []*user_model.User) {
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
db.DeleteByID[user_model.User](ctx, u.ID)
|
if u.IsOrganization() {
|
||||||
|
organization_model.DeleteOrganization(ctx, (*organization_model.Organization)(u))
|
||||||
|
} else {
|
||||||
|
db.DeleteByID[user_model.User](ctx, u.ID)
|
||||||
|
db.DeleteByBean(ctx, &user_model.EmailAddress{UID: u.ID})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue