Stop reporting a failed login lookup as "Invalid Email"

GetUserByAuthname / GetUserByContactNo / GetUserLogin discarded the
Scan error, so a database that could not answer — down, pool exhausted,
or booted without its config (2026-07-20) — came back as uid 0 and every
user was told their email was wrong.

One lookup, GetUserLogin, now returns an error; sql.ErrNoRows is "not
found" and anything else reaches the service, which answers 500 "Login
is temporarily unavailable" and logs the cause. 409 "Invalid Email" is
unchanged for a genuine no-match: the console reads that exact shape as
"not registered". NULL password/role columns scan through sql.Null* so
they do not become 500s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-15 17:04:33 +05:30
parent 4474479735
commit 1633617dc4
4 changed files with 249 additions and 68 deletions

View File

@@ -2,6 +2,7 @@ package services
import (
"errors"
"log"
"nearle/models"
"nearle/repositories"
"strings"
@@ -9,6 +10,48 @@ import (
"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)
@@ -80,15 +123,7 @@ func (s *userService) UpdateStaff(user models.User) error {
}
func (s *userService) AppLogin(user models.User) (models.TenantUserInfo, fiber.Map, error) {
var uid int
var status, dbPassword string
// Get user by authname or contactno
if user.Authname != "" {
uid, dbPassword, status = s.repo.GetUserByAuthname(user.Authname, user.Configid)
} else if user.Contactno != "" {
uid, dbPassword, status = s.repo.GetUserByContactNo(user.Contactno, user.Configid)
} else {
if user.Authname == "" && user.Contactno == "" {
resp := fiber.Map{
"code": 400,
"status": false,
@@ -97,7 +132,12 @@ func (s *userService) AppLogin(user models.User) (models.TenantUserInfo, fiber.M
return models.TenantUserInfo{}, resp, errors.New("missing authname or contactno")
}
// Invalid user
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,
@@ -212,14 +252,8 @@ func (s *userService) CreateUser(user models.User) (models.UserInfo, error) {
func (s *userService) TenantWebLogin(user models.User) (models.TenantUserInfo, map[string]interface{}) {
tenantFormExists := true
uid, dbPassword, status, roleid := 0, "", "", 0
// Step 1: Login by authname or contactno
if user.Authname != "" {
uid, dbPassword, status, roleid = s.repo.GetUserLogin("authname", user.Authname, user.Configid)
} else if user.Contactno != "" {
uid, dbPassword, status, roleid = s.repo.GetUserLogin("contactno", user.Contactno, user.Configid)
} else {
if user.Authname == "" && user.Contactno == "" {
return models.TenantUserInfo{}, map[string]interface{}{
"status": true,
"code": 400,
@@ -227,7 +261,13 @@ func (s *userService) TenantWebLogin(user models.User) (models.TenantUserInfo, m
}
}
// Step 2: Validate user
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,