Files
backend_fiesta/services/userService.go
2026-09-25 10:36:38 +05:30

384 lines
12 KiB
Go

package services
import (
"errors"
"fmt"
"log"
"nearle/models"
"nearle/repositories"
"strings"
"github.com/gofiber/fiber"
)
// errLoginUnavailable is returned by lookupLogin when the database could not
// answer the sign-in query. It is deliberately not "invalid user": the account
// may well exist, and the person needs to be told to try again, not to check
// their spelling.
var errLoginUnavailable = errors.New("login lookup failed")
// loginUnavailableResponse is the body for that case. 500 rather than 409,
// because the console reads `409` as "this email does not exist" (see
// daily_merchant_web/src/services/auth.ts) and would otherwise send a
// perfectly good account to the sign-up form while the database was down.
func loginUnavailableResponse() map[string]interface{} {
return map[string]interface{}{
"status": false,
"code": 500,
"message": "Login is temporarily unavailable. Please try again in a moment.",
}
}
// lookupLogin resolves who is signing in, by authname first and contact number
// second, and separates "not found" from "could not look".
//
// Both login paths used to run their own copy of this and both discarded the
// repository's error, so a database that was down came back as uid 0 and was
// reported as "Invalid Email". That message now means exactly one thing: the
// query ran and matched nobody.
func (s *userService) lookupLogin(user models.User) (uid int, password, status string, roleid int, err error) {
field, value := "authname", user.Authname
if user.Authname == "" {
field, value = "contactno", user.Contactno
}
uid, password, status, roleid, err = s.repo.GetUserLogin(field, value, user.Configid)
if err != nil {
// The value is what the caller typed — an email or a phone number —
// and is safe to log; the password never reaches this function's
// output.
log.Printf("login: lookup by %s=%q configid=%d failed: %v", field, value, user.Configid, err)
return 0, "", "", 0, errLoginUnavailable
}
return uid, password, status, roleid, nil
}
type UserService interface {
GetAllUsers(roleID, tenantID, pageno, pagesize int, keyword string) ([]models.UserInfo, error)
GetUserByID(uid int) (models.UserInfo, error)
Login(user models.User) (models.UserInfo, error)
TenantLogin(user models.User) (models.TenantUserInfo, error)
UpdateStaff(user models.User) error
// SetInitialPassword is the one write reachable without a session — see
// 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)
TenantWebLogin(user models.User) (models.TenantUserInfo, map[string]interface{})
DeleteUser(userid int) error
}
type userService struct {
repo repositories.UserRepository
}
func NewUserService(repo repositories.UserRepository) UserService {
return &userService{repo: repo}
}
func (s *userService) GetAllUsers(roleID, tenantID, pageno, pagesize int, keyword string) ([]models.UserInfo, error) {
return s.repo.GetAllUsers(roleID, tenantID, pageno, pagesize, keyword)
}
func (s *userService) GetUserByID(uid int) (models.UserInfo, error) {
return s.repo.GetUserByID(uid)
}
func (s *userService) Login(user models.User) (models.UserInfo, error) {
return s.repo.Login(user)
}
func (s *userService) TenantLogin(user models.User) (models.TenantUserInfo, error) {
uid, err := s.repo.FindUserID(user.Authname, user.Contactno, user.Configid)
if err != nil {
return models.TenantUserInfo{}, err
}
if uid == 0 {
return models.TenantUserInfo{}, errors.New("user not found")
}
// `GetTenantUserById`, NOT `GetTenantUserByID`.
//
// The two differ by one letter and by twenty-eight columns. The capital-ID
// one selects five — userid, authname, contactno, tenantid, tenantname —
// so this function used to answer with a record whose name, branch, region
// and coordinates were all blank. That is why routing
// `/mob/users/tenant/login` at it was never as simple as swapping the
// handler: the app would have received a mostly-empty object.
//
// The lowercase-d one is the query `AppLogin` and `TenantWebLogin` already
// use, and it fills the whole record. It is now the only one anything calls.
//
// The FCM token is stored here rather than left to the repository, because
// the full read does not write. Only a real token is stored: a login that
// omits it must not wipe the device already registered, which is exactly
// what "userfcmtoken": "your_fcm_token_here" did to a live account during
// this investigation.
if strings.TrimSpace(user.Userfcmtoken) != "" {
_ = s.repo.UpdateUserFcmToken(uid, user.Userfcmtoken)
}
return s.repo.GetTenantUserById(uid), nil
}
func (s *userService) UpdateStaff(user models.User) error {
return s.repo.UpdateStaff(user)
}
func (s *userService) AppLogin(user models.User) (models.TenantUserInfo, fiber.Map, error) {
if user.Authname == "" && user.Contactno == "" {
resp := fiber.Map{
"code": 400,
"status": false,
"message": "authname or contactno required",
}
return models.TenantUserInfo{}, resp, errors.New("missing authname or contactno")
}
uid, dbPassword, status, _, err := s.lookupLogin(user)
if err != nil {
return models.TenantUserInfo{}, fiber.Map(loginUnavailableResponse()), err
}
// Nobody matched. This is the only way to reach "Invalid Email" now.
if uid == 0 {
resp := fiber.Map{
"status": false,
"code": 409,
"message": "Invalid Email",
"tenantform": true,
}
return models.TenantUserInfo{}, resp, errors.New("invalid user")
}
// Inactive account
if strings.EqualFold(status, "InActive") {
resp := fiber.Map{
"status": false,
"code": 403,
"message": "Inactive Account. Contact admin.",
}
return models.TenantUserInfo{}, resp, errors.New("inactive account")
}
// No password set
if strings.TrimSpace(dbPassword) == "" {
resp := fiber.Map{
"status": true,
"code": 409,
"message": "Please setup a password.",
"tenantform": true,
"details": fiber.Map{
"userid": uid,
"setup": true,
},
}
return models.TenantUserInfo{}, resp, nil
}
// Empty request password
if strings.TrimSpace(user.Password) == "" {
resp := fiber.Map{
"status": true,
"code": 401,
"message": "Password is required",
"tenantform": true,
}
return models.TenantUserInfo{}, resp, nil
}
// Incorrect password
if user.Password != dbPassword {
resp := fiber.Map{
"status": false,
"code": 401,
"message": "Incorrect password",
"tenantform": true,
}
return models.TenantUserInfo{}, resp, errors.New("incorrect password")
}
// Update FCM token
if user.Userfcmtoken != "" {
_ = s.repo.UpdateFCMToken(uid, user.Userfcmtoken)
}
// ✅ Fetch tenant user info
info := s.repo.GetTenantUserById(uid)
// ✅ Check if assigned store is inactive
if info.Locationid > 0 {
storeStatus := s.repo.GetLocationStatus(info.Locationid)
if strings.EqualFold(storeStatus, "InActive") {
resp := fiber.Map{
"status": false,
"code": 403,
"message": "Assigned store is inactive. Contact admin.",
}
return models.TenantUserInfo{}, resp, errors.New("inactive store")
}
}
// ✅ Return success response
resp := fiber.Map{
"status": true,
"code": 200,
"message": "Login successful",
"details": info,
}
return info, resp, nil
}
func (s *userService) CreateUser(user models.User) (models.UserInfo, 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
// column. See PrepareNewAccount.
user = PrepareNewAccount(user)
// Call repository to create user
userid, err := s.repo.CreateUser(user)
if err != nil {
return models.UserInfo{}, err
}
// Get user info by id
info, err := s.repo.GetUserById(userid)
if err != nil {
return models.UserInfo{}, err
}
return info, nil
}
func (s *userService) TenantWebLogin(user models.User) (models.TenantUserInfo, map[string]interface{}) {
tenantFormExists := true
// Step 1: Login by authname or contactno
if user.Authname == "" && user.Contactno == "" {
return models.TenantUserInfo{}, map[string]interface{}{
"status": true,
"code": 400,
"message": "authname or contactno required",
}
}
uid, dbPassword, status, roleid, err := s.lookupLogin(user)
if err != nil {
return models.TenantUserInfo{}, loginUnavailableResponse()
}
// Step 2: Validate user. Nobody matched — the only way to reach
// "Invalid Email" now; a database that could not answer is a 500 above.
if uid == 0 {
return models.TenantUserInfo{}, map[string]interface{}{
"status": false,
"code": 409,
"message": "Invalid Email",
"tenantform": tenantFormExists,
}
}
if strings.EqualFold(status, "InActive") {
return models.TenantUserInfo{}, map[string]interface{}{
"status": false,
"code": 403,
"message": "Inactive Account. Contact admin.",
}
}
if user.Roleid != roleid {
return models.TenantUserInfo{}, map[string]interface{}{
"status": false,
"code": 403,
"message": "Unauthorized email.",
}
}
// Step 3: Password checks
if strings.TrimSpace(dbPassword) == "" {
return models.TenantUserInfo{}, map[string]interface{}{
"status": true,
"code": 409,
"message": "Please setup a password.",
"tenantform": tenantFormExists,
"details": map[string]interface{}{
"userid": uid,
"setup": true,
},
}
}
if strings.TrimSpace(user.Password) == "" {
return models.TenantUserInfo{}, map[string]interface{}{
"status": true,
"code": 401,
"message": "Password is required",
"tenantform": tenantFormExists,
}
}
if user.Password != dbPassword {
return models.TenantUserInfo{}, map[string]interface{}{
"status": false,
"code": 401,
"message": "Incorrect password",
"tenantform": tenantFormExists,
}
}
// Step 4: Update FCM if provided
if user.Userfcmtoken != "" {
_ = s.repo.UpdateUserFcmToken(uid, user.Userfcmtoken)
}
// Step 5: Get full tenant info
info := s.repo.GetTenantUserById(uid)
// Step 6: Check if assigned store is inactive
if info.Locationid > 0 {
storeStatus := s.repo.GetLocationStatus(info.Locationid)
if strings.EqualFold(storeStatus, "InActive") {
return models.TenantUserInfo{}, map[string]interface{}{
"status": false,
"code": 403,
"message": "Assigned store is inactive. Contact admin.",
}
}
}
return info, map[string]interface{}{
"status": true,
"code": 200,
"message": "Login successful",
}
}
func (s *userService) DeleteUser(userid int) error {
return s.repo.DeleteUser(userid)
}
// SetInitialPassword gives a never-used account its first password.
//
// The minimum length is enforced here as well as at the edge: this is the only
// write in the product reachable without a session, so the rule cannot live
// only in a handler that a second caller might not go through.
func (s *userService) SetInitialPassword(userid int, password string) error {
if userid <= 0 {
return errors.New("which account?")
}
password = strings.TrimSpace(password)
if len(password) < MinPasswordLength {
return fmt.Errorf("the password must be at least %d characters", MinPasswordLength)
}
return s.repo.SetInitialPassword(userid, password)
}
// MinPasswordLength is the floor for a console password.
//
// Six, matching the check `UpdateStaff` already applied at the controller — not
// a new rule, the same one stated where both callers can reach it.
const MinPasswordLength = 6