auto mail generation
This commit is contained in:
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user