384 lines
12 KiB
Go
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
|