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

@@ -255,7 +255,7 @@ func (ctl *UserController) CreateUser(c *fiber.Ctx) error {
}
// Call service
info, err := ctl.userService.CreateUser(user)
info, invite, err := ctl.userService.CreateUser(user)
if err != nil {
return c.Status(http.StatusConflict).JSON(fiber.Map{
"code": http.StatusConflict,
@@ -264,11 +264,17 @@ func (ctl *UserController) CreateUser(c *fiber.Ctx) error {
})
}
// The account was created either way. Whether its first-password invitation
// was emailed is reported beside it rather than folded into `status`: the
// account has no password and the link is the only way to set one, so an
// operator who is not told has hired somebody who cannot sign in.
return c.Status(http.StatusCreated).JSON(fiber.Map{
"code": http.StatusCreated,
"status": true,
"message": "Success",
"details": info,
"code": http.StatusCreated,
"status": true,
"message": "Success",
"details": info,
"invited": invite.Sent,
"invitereason": invite.Reason,
})
}
@@ -347,7 +353,9 @@ func (ctl *UserController) DeleteUser(c *fiber.Ctx) error {
// what makes it safe to leave open. See the repository for the rest.
func (ctl *UserController) SetPassword(c *fiber.Ctx) error {
var req struct {
Userid int `json:"userid"`
// The invitation, exactly as it arrived in the emailed link. The userid
// is read out of the signature and never out of the request — see below.
Token string `json:"token"`
Password string `json:"password"`
}
if err := c.BodyParser(&req); err != nil {
@@ -356,7 +364,27 @@ func (ctl *UserController) SetPassword(c *fiber.Ctx) error {
})
}
if err := ctl.userService.SetInitialPassword(req.Userid, req.Password); err != nil {
// ── Why this takes a token and no longer takes a userid ─────────────────
//
// It used to accept `{userid, password}`, and that was an account takeover
// waiting to be noticed. `applogin` answers a POST carrying an email and NO
// password with 409 and the userid, for any account that has not set one —
// which is how the console's own setup step learned it. So the whole recipe
// was: know a merchant's primary email, which is usually printed on their
// shopfront, POST it here, receive their userid, then set their password
// and own the business's admin account. No guessing at any step.
//
// The invitation closes it. It is signed with the deployment's key, names
// the account in a payload the server produced, and expires. Knowing an
// email is no longer enough, and neither is knowing a userid.
claims, err := utils.ParseInviteToken(req.Token, time.Now())
if err != nil {
return c.Status(http.StatusConflict).JSON(fiber.Map{
"status": false, "code": http.StatusConflict, "message": err.Error(),
})
}
if err := ctl.userService.SetInitialPassword(claims.Userid, req.Password); err != nil {
// 409, not 401. Nothing about this is an authentication failure — the
// caller is not supposed to have a session — and answering 401 would
// send the console into its sign-out-and-reload path on the one screen