auto mail generation

This commit is contained in:
2026-09-29 16:53:21 +05:30
parent b18080d429
commit b46902f51b
27 changed files with 2765 additions and 80 deletions

View File

@@ -63,17 +63,25 @@ type UserService interface {
// the repository for what makes that safe.
SetInitialPassword(userid int, password string) error
AppLogin(user models.User) (models.TenantUserInfo, fiber.Map, error)
CreateUser(user models.User) (models.UserInfo, error)
// Creates a back-office account and emails its first-password invitation.
//
// The outcome travels beside the user rather than as an error: the person is
// hired either way, and whether the mail left is something the console shows
// so somebody can resend it.
CreateUser(user models.User) (models.UserInfo, InviteOutcome, error)
TenantWebLogin(user models.User) (models.TenantUserInfo, map[string]interface{})
DeleteUser(userid int) error
}
type userService struct {
repo repositories.UserRepository
// May be nil, like the tenant service's. A deployment with no mail still
// creates accounts; the outcome names the missing variable.
invites InviteService
}
func NewUserService(repo repositories.UserRepository) UserService {
return &userService{repo: repo}
func NewUserService(repo repositories.UserRepository, invites InviteService) UserService {
return &userService{repo: repo, invites: invites}
}
func (s *userService) GetAllUsers(roleID, tenantID, pageno, pagesize int, keyword string) ([]models.UserInfo, error) {
@@ -162,16 +170,35 @@ func (s *userService) AppLogin(user models.User) (models.TenantUserInfo, fiber.M
return models.TenantUserInfo{}, resp, errors.New("inactive account")
}
// No password set
// No password set.
//
// ── The userid used to be in here, and that was the whole exploit ───────
//
// This branch is reached by a POST carrying an email and NO password, so
// anyone could ask it about any account. It answered with the userid, and
// `setpassword` then took a bare userid — so the recipe was: read a
// merchant's primary email off their shopfront, POST it here, receive their
// userid, set their password, own the business's admin account. No guessing
// at any step.
//
// `setpassword` now requires a signed invitation, so the userid alone is no
// longer a way in. It is still removed, because handing it out told an
// unauthenticated caller which businesses exist and which have never been
// set up — a list worth having if you are the one sending the phishing
// email that arrives before the real invitation does.
//
// The message is kept deliberately vague for the same reason. "Please set
// up a password" invited the caller to do exactly that; this says where the
// link comes from instead, which is true for the person who belongs here
// and useless to anyone else.
if strings.TrimSpace(dbPassword) == "" {
resp := fiber.Map{
"status": true,
"code": 409,
"message": "Please setup a password.",
"message": "This account has not been set up yet. Use the invitation link that was emailed to you.",
"tenantform": true,
"details": fiber.Map{
"userid": uid,
"setup": true,
"setup": true,
},
}
return models.TenantUserInfo{}, resp, nil
@@ -231,7 +258,7 @@ func (s *userService) AppLogin(user models.User) (models.TenantUserInfo, fiber.M
return info, resp, nil
}
func (s *userService) CreateUser(user models.User) (models.UserInfo, error) {
func (s *userService) CreateUser(user models.User) (models.UserInfo, InviteOutcome, error) {
// Without an authname and a console configid the account is created,
// listed, and then refused at the login screen: `weblogin` matches
// `WHERE authname = ? AND configid = ?` and never looks at the email
@@ -241,16 +268,53 @@ func (s *userService) CreateUser(user models.User) (models.UserInfo, error) {
// Call repository to create user
userid, err := s.repo.CreateUser(user)
if err != nil {
return models.UserInfo{}, err
return models.UserInfo{}, InviteOutcome{}, err
}
// Get user info by id
info, err := s.repo.GetUserById(userid)
if err != nil {
return models.UserInfo{}, err
return models.UserInfo{}, InviteOutcome{}, err
}
return info, nil
// The invitation, after the write and outside it.
//
// This account is created with NO password — nothing on this path sets one —
// and since the sign-in screen stopped offering to set a first password, the
// emailed link is the only way in. Without this the person is added to the
// directory, appears in every branch picker, and cannot sign in, with nothing
// anywhere to say why.
//
// `info.Userid` rather than `userid`: identical, but this is the row that was
// actually read back, so an invitation is never addressed to an id the
// database did not confirm.
return info, s.inviteNewAccount(info, user), nil
}
// inviteNewAccount emails the person who was just hired.
//
// Never an error. A failure is the operator's task — resend, or fix the address —
// and not a reason to unwind a hire that has already happened.
func (s *userService) inviteNewAccount(info models.UserInfo, user models.User) InviteOutcome {
if s.invites == nil {
return InviteOutcome{Reason: "invitations are not configured on this server"}
}
if info.Userid <= 0 {
return InviteOutcome{Reason: "the new account could not be read back to invite it"}
}
// The authname IS the email on a back-office account — `PrepareNewAccount`
// copies one to the other — so this is a fallback for a caller that filled in
// only one of the two, never a second address.
address := strings.TrimSpace(user.Email)
if address == "" {
address = strings.TrimSpace(user.Authname)
}
// Empty business name: the invite service reads it from the tenantid. A staff
// row carries the id and nothing else about the business.
sent, reason := s.invites.Invite(info.Userid, user.Tenantid, address, "")
return InviteOutcome{Sent: sent, Reason: reason}
}
func (s *userService) TenantWebLogin(user models.User) (models.TenantUserInfo, map[string]interface{}) {
@@ -297,16 +361,19 @@ func (s *userService) TenantWebLogin(user models.User) (models.TenantUserInfo, m
}
}
// Step 3: Password checks
// Step 3: Password checks.
//
// The userid is withheld here for the same reason as in `AppLogin` above:
// this branch answers an unauthenticated caller asking about an email, and
// the userid was half of an account takeover. See the long note there.
if strings.TrimSpace(dbPassword) == "" {
return models.TenantUserInfo{}, map[string]interface{}{
"status": true,
"code": 409,
"message": "Please setup a password.",
"message": "This account has not been set up yet. Use the invitation link that was emailed to you.",
"tenantform": tenantFormExists,
"details": map[string]interface{}{
"userid": uid,
"setup": true,
"setup": true,
},
}
}