374 lines
11 KiB
Go
374 lines
11 KiB
Go
package controllers
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"nearle/models"
|
|
"nearle/services"
|
|
"nearle/utils"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// attachWebSession hands a signed-in console user their session token.
|
|
//
|
|
// Added to the login response rather than served from a second endpoint, so the
|
|
// console receives it on the call it already makes and nothing changes about
|
|
// when or how it signs in.
|
|
//
|
|
// The claims come from the user's own record, which is the whole point: until
|
|
// now the console asserted its tenant on every request and was believed, and
|
|
// sealing it under a signature here is what makes `middleware.WebAuth` able to
|
|
// refuse a request naming somebody else's.
|
|
//
|
|
// `Issuperadmin` is copied across as the ONLY source of cross-tenant access.
|
|
// Not the role — `app_roles` calls roleid 1 "Super admin" and tenant onboarding
|
|
// wrote 1 for every shop owner, so trusting the role would promote every
|
|
// merchant on the platform.
|
|
//
|
|
// A failure to mint is logged and swallowed, deliberately, while
|
|
// WEB_AUTH_REQUIRED is off: a deployment that has not set a signing key yet must
|
|
// still be able to sign in, or shipping this takes the console down everywhere
|
|
// the secret is missing. Once enforcement is on, no token means no session —
|
|
// which is then the correct and loud failure.
|
|
//
|
|
// The parameter is the underlying map type rather than `fiber.Map`, because the
|
|
// two login paths do not agree on which fiber that is: `AppLogin` returns the
|
|
// v1 package's `Map` and `TenantWebLogin` the v2 one. Both are
|
|
// `map[string]any`, so taking that accepts either without dragging the
|
|
// old import into this file.
|
|
func attachWebSession(resp map[string]any, info models.TenantUserInfo) {
|
|
token, expires, err := utils.MintWebToken(utils.WebClaims{
|
|
Userid: info.Userid,
|
|
Tenantid: info.Tenantid,
|
|
Locationid: info.Locationid,
|
|
Roleid: info.Roleid,
|
|
Configid: info.Configid,
|
|
Superadmin: info.Issuperadmin,
|
|
}, time.Now())
|
|
if err != nil {
|
|
log.Printf("login: could not issue a console session for user %d: %v", info.Userid, err)
|
|
return
|
|
}
|
|
resp["token"] = token
|
|
resp["tokenexpiresat"] = expires.Unix()
|
|
}
|
|
|
|
type UserController struct {
|
|
userService services.UserService
|
|
}
|
|
|
|
func NewUserController(userService services.UserService) *UserController {
|
|
return &UserController{userService: userService}
|
|
}
|
|
|
|
func (ctl *UserController) GetAllUsers(c *fiber.Ctx) error {
|
|
roleID, _ := strconv.Atoi(c.Query("roleid", "0"))
|
|
tenantID, _ := strconv.Atoi(c.Query("tenantid", "0"))
|
|
pageno, _ := strconv.Atoi(c.Query("pageno", "1"))
|
|
pagesize, _ := strconv.Atoi(c.Query("pagesize", "10"))
|
|
keyword := c.Query("keyword", "")
|
|
|
|
if tenantID == 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "tenantid is required",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
users, err := ctl.userService.GetAllUsers(roleID, tenantID, pageno, pagesize, keyword)
|
|
if err != nil {
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": users,
|
|
})
|
|
}
|
|
|
|
func (ctl *UserController) GetUserInfo(c *fiber.Ctx) error {
|
|
uid, err := strconv.Atoi(c.Query("userid"))
|
|
if err != nil || uid <= 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid userid",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
user, err := ctl.userService.GetUserByID(uid)
|
|
if err != nil {
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": user,
|
|
})
|
|
}
|
|
|
|
func (ctl *UserController) Login(c *fiber.Ctx) error {
|
|
var user models.User
|
|
if err := c.BodyParser(&user); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid request body",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
info, err := ctl.userService.Login(user)
|
|
if err != nil {
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusConflict,
|
|
"message": "User not found",
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": info,
|
|
})
|
|
}
|
|
|
|
func (ctl *UserController) TenantLogin(c *fiber.Ctx) error {
|
|
var user models.User
|
|
if err := c.BodyParser(&user); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
info, err := ctl.userService.TenantLogin(user)
|
|
if err != nil {
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusConflict,
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": info,
|
|
})
|
|
}
|
|
|
|
func (ctl *UserController) UpdateStaff(c *fiber.Ctx) error {
|
|
var user models.User
|
|
if err := c.BodyParser(&user); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
// This endpoint also doubles as the password-setup/reset call (userid +
|
|
// password only, everything else left zero so GORM's Updates skips it) —
|
|
// guard the one field that has no validation anywhere else on this path.
|
|
if pw := strings.TrimSpace(user.Password); pw != "" && len(pw) < 6 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusBadRequest,
|
|
"message": "Password must be at least 6 characters",
|
|
})
|
|
}
|
|
|
|
if err := ctl.userService.UpdateStaff(user); err != nil {
|
|
return c.JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusConflict,
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"status": true,
|
|
"code": http.StatusAccepted,
|
|
"message": "User update successful",
|
|
})
|
|
}
|
|
|
|
func (ctl *UserController) AppLogin(c *fiber.Ctx) error {
|
|
var user models.User
|
|
|
|
if err := c.BodyParser(&user); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"code": 400,
|
|
"status": false,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
info, resp, err := ctl.userService.AppLogin(user)
|
|
if err != nil {
|
|
// Use resp.Code if present, fallback to 409
|
|
code := http.StatusConflict
|
|
if v, ok := resp["code"].(int); ok {
|
|
code = v
|
|
}
|
|
return c.Status(code).JSON(resp)
|
|
}
|
|
|
|
attachWebSession(resp, info)
|
|
|
|
// ✅ Always return resp
|
|
return c.Status(http.StatusOK).JSON(resp)
|
|
}
|
|
|
|
func (ctl *UserController) CreateUser(c *fiber.Ctx) error {
|
|
var user models.User
|
|
|
|
// Parse request body
|
|
if err := c.BodyParser(&user); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"status": false,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
// Call service
|
|
info, err := ctl.userService.CreateUser(user)
|
|
if err != nil {
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"code": http.StatusConflict,
|
|
"status": false,
|
|
"message": "Failed",
|
|
})
|
|
}
|
|
|
|
return c.Status(http.StatusCreated).JSON(fiber.Map{
|
|
"code": http.StatusCreated,
|
|
"status": true,
|
|
"message": "Success",
|
|
"details": info,
|
|
})
|
|
}
|
|
|
|
func (ctl *UserController) TenantWebLogin(c *fiber.Ctx) error {
|
|
var user models.User
|
|
if err := c.BodyParser(&user); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": fiber.StatusBadRequest,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
info, resp := ctl.userService.TenantWebLogin(user)
|
|
|
|
// Ensure the response map contains the correct status code
|
|
code, ok := resp["code"].(int)
|
|
if !ok {
|
|
code = fiber.StatusInternalServerError
|
|
}
|
|
|
|
// Include tenant user info if login successful (code 200)
|
|
if code == fiber.StatusOK {
|
|
resp["details"] = info
|
|
attachWebSession(resp, info)
|
|
}
|
|
|
|
return c.Status(code).JSON(resp)
|
|
}
|
|
|
|
func (ctl *UserController) DeleteUser(c *fiber.Ctx) error {
|
|
uid, err := strconv.Atoi(c.Query("userid"))
|
|
if err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid user ID",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
if err := ctl.userService.DeleteUser(uid); err != nil {
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "User successfully deleted",
|
|
"status": true,
|
|
})
|
|
}
|
|
|
|
// SetPassword gives a never-used account its first password.
|
|
//
|
|
// ── Why this endpoint exists ────────────────────────────────────────────────
|
|
//
|
|
// Because the flow was impossible without it. A branch login created by
|
|
// `createtenantlocation` arrives with an empty password; the console signs in,
|
|
// is told to set one, and does so — through `PUT /users/update`, which sits
|
|
// behind the session guard. So the call answered "a session token is required;
|
|
// sign in again" to a person who could not sign in, because they had no
|
|
// password yet. Every such account was unusable.
|
|
//
|
|
// `publicWebPaths` has named `/users/setpassword` since the guard was written.
|
|
// The path was reserved and the handler never built, so it answered 404 and the
|
|
// console went on using the guarded one.
|
|
//
|
|
// ── Why not simply open up `/users/update` ──────────────────────────────────
|
|
//
|
|
// It writes whatever struct it is handed. Unauthenticated, it would let anybody
|
|
// change any field of any user — their email, their role, their tenant. This
|
|
// takes two fields and can only act on an account with no password, which is
|
|
// 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"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"status": false, "code": http.StatusBadRequest, "message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
if err := ctl.userService.SetInitialPassword(req.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
|
|
// where there is nothing to sign out of.
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"status": false, "code": http.StatusConflict, "message": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"status": true, "code": http.StatusOK,
|
|
"message": "Password set. Sign in with it.",
|
|
})
|
|
}
|