feat: miler self-set PIN on first login; no console-set default PIN
Milers now choose their own PIN the first time they log in, instead of the console assigning a shared default: - CreateMiler always creates a rider with an empty Password (PIN field removed from MilerCreateRequest); any client-supplied PIN is ignored, making "riders set their own PIN" a backend invariant, not a console convention. - LoginMiler returns `pin_set` so the app routes to enter-PIN vs set-PIN. - New POST /miler/set-pin (SetMilerPin): self-service first PIN, allowed ONLY when the account has none yet (409 otherwise, so it can't overwrite/take over an active account), then logs the rider in. Self-service and throttle-only is safe because of that guard; OTP-gate it once the SMS gateway is live. - verify-pin and set-pin share issueMilerSession so the two success responses can't drift. Also switches BookingPickupComplete's timestamp to DBNow() (IST) so the compatibility-flow inwardedat matches the reconciliation windows. Existing riders keep their PIN and are unaffected; blanking their password to move them onto self-set is a separate, deliberate DB step. go build, go vet and go test ./... all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRaFH5hMRqmUQvVPQsyjZD
This commit is contained in:
@@ -1859,8 +1859,6 @@ func CreateMiler(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
passHash, _ := utils.HashPassword(req.Password)
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
appLocID := req.Applocationid
|
||||
@@ -1884,10 +1882,12 @@ func CreateMiler(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
user := models.AppUser{
|
||||
Authname: req.Authname,
|
||||
Email: req.Email,
|
||||
Contactno: req.Contactno,
|
||||
Password: passHash,
|
||||
Authname: req.Authname,
|
||||
Email: req.Email,
|
||||
Contactno: req.Contactno,
|
||||
// Empty PIN by design: the rider self-sets it on first login via
|
||||
// /miler/set-pin. See MilerCreateRequest — no console-set PIN.
|
||||
Password: "",
|
||||
Roleid: 5, // Miler
|
||||
Status: "Active",
|
||||
Applocationid: appLocID,
|
||||
|
||||
@@ -55,10 +55,14 @@ func LoginMiler(cfg *config.Config) fiber.Handler {
|
||||
return utils.Forbidden(c, "miler account is not active")
|
||||
}
|
||||
|
||||
// pin_set tells the app which screen to show next: true → enter-PIN
|
||||
// (verify-pin login), false → set-PIN (first-time). A rider created without
|
||||
// a PIN has an empty Password; every real PIN is a non-empty bcrypt hash.
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"message": "PIN verification required",
|
||||
"phone": req.Phone,
|
||||
"pin_set": user.Password != "",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -113,49 +117,57 @@ func VerifyMilerPin(cfg *config.Config) fiber.Handler {
|
||||
return utils.Unauthorized(c, "incorrect PIN")
|
||||
}
|
||||
|
||||
token, err := utils.GenerateToken(user.Userid, user.Email, user.Roleid, user.Tenantid, user.Configid, cfg.JWTSecret)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to generate token")
|
||||
}
|
||||
return issueMilerSession(c, cfg, user, req.DeviceToken)
|
||||
}
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", user.Userid).First(&profile).Error; err != nil {
|
||||
profile = models.MilerProfile{
|
||||
Userid: user.Userid,
|
||||
Displayname: user.Authname,
|
||||
Phone: user.Contactno,
|
||||
Availabilitystatus: constants.MilerOffline,
|
||||
Rating: 5.0,
|
||||
Applocationid: user.Applocationid,
|
||||
}
|
||||
db.DB.Create(&profile)
|
||||
}
|
||||
if req.DeviceToken != "" && profile.Devicetoken != req.DeviceToken {
|
||||
profile.Devicetoken = req.DeviceToken
|
||||
db.DB.Model(&profile).Update("device_token", req.DeviceToken)
|
||||
}
|
||||
// issueMilerSession builds the authenticated-session response shared by
|
||||
// verify-pin and set-pin: it mints the JWT, ensures a MilerProfile exists,
|
||||
// refreshes the device token, and returns the single {token, user, profile}
|
||||
// shape both entry points must agree on. tenantname drives the app's
|
||||
// service-profile resolution (hyperlocal vs logistics), checked ahead of the
|
||||
// raw tenantid and persisted client-side so a rider picks up a changed tenant
|
||||
// name on next login.
|
||||
func issueMilerSession(c *fiber.Ctx, cfg *config.Config, user models.AppUser, deviceToken string) error {
|
||||
token, err := utils.GenerateToken(user.Userid, user.Email, user.Roleid, user.Tenantid, user.Configid, cfg.JWTSecret)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to generate token")
|
||||
}
|
||||
|
||||
// tenantname drives the app's service-profile resolution (hyperlocal vs
|
||||
// logistics), checked ahead of the raw tenantid. Persisted client-side at
|
||||
// verify-pin, so a rider picks up a changed tenant name on next login.
|
||||
tenantName := resolveTenantName(user.Tenantid)
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", user.Userid).First(&profile).Error; err != nil {
|
||||
profile = models.MilerProfile{
|
||||
Userid: user.Userid,
|
||||
Displayname: user.Authname,
|
||||
Phone: user.Contactno,
|
||||
Availabilitystatus: constants.MilerOffline,
|
||||
Rating: 5.0,
|
||||
Applocationid: user.Applocationid,
|
||||
}
|
||||
db.DB.Create(&profile)
|
||||
}
|
||||
if deviceToken != "" && profile.Devicetoken != deviceToken {
|
||||
profile.Devicetoken = deviceToken
|
||||
db.DB.Model(&profile).Update("device_token", deviceToken)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"token": token,
|
||||
tenantName := resolveTenantName(user.Tenantid)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"token": token,
|
||||
"tenantid": user.Tenantid,
|
||||
"tenantname": tenantName,
|
||||
"user": fiber.Map{
|
||||
"userid": user.Userid,
|
||||
"authname": user.Authname,
|
||||
"email": user.Email,
|
||||
"contactno": user.Contactno,
|
||||
"tenantid": user.Tenantid,
|
||||
"tenantname": tenantName,
|
||||
"user": fiber.Map{
|
||||
"userid": user.Userid,
|
||||
"authname": user.Authname,
|
||||
"email": user.Email,
|
||||
"contactno": user.Contactno,
|
||||
"tenantid": user.Tenantid,
|
||||
"tenantname": tenantName,
|
||||
"profile": profile,
|
||||
},
|
||||
})
|
||||
}
|
||||
"profile": profile,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ResetMilerPin lets a miler who forgot their PIN set a new one from just
|
||||
@@ -200,6 +212,57 @@ func ResetMilerPin(c *fiber.Ctx) error {
|
||||
return utils.Message(c, "PIN reset successfully")
|
||||
}
|
||||
|
||||
// SetMilerPin is the self-service first-login PIN creation, the counterpart to
|
||||
// the ops-only ResetMilerPin. It is allowed ONLY when the account has no PIN yet
|
||||
// (empty Password): because it can never overwrite an existing PIN, the
|
||||
// phone-only unauthenticated path here cannot take over an already-active
|
||||
// account the way an unguarded reset could — the worst case is a not-yet-used
|
||||
// account being claimed by someone who knows the phone number, which OTP should
|
||||
// close once the SMS gateway is live. A rider who already has a PIN is sent to
|
||||
// verify-pin (or an ops reset) via 409. On success the rider is logged in
|
||||
// immediately, so the app never has to make a second verify-pin call.
|
||||
func SetMilerPin(cfg *config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
req := new(dto.MilerSetPinRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if req.Phone == "" || req.NewPin == "" {
|
||||
return utils.BadRequest(c, "phone and new_pin are required")
|
||||
}
|
||||
|
||||
configID := req.Configid
|
||||
if configID == 0 {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("contactno = ? AND configid = ?", req.Phone, configID).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "no miler account found for this phone number")
|
||||
}
|
||||
if user.Roleid != 5 {
|
||||
return utils.Forbidden(c, "this endpoint is restricted to miler accounts")
|
||||
}
|
||||
if user.Status != "Active" {
|
||||
return utils.Forbidden(c, "miler account is not active")
|
||||
}
|
||||
if user.Password != "" {
|
||||
return utils.Conflict(c, "a PIN is already set for this account; use verify-pin to log in")
|
||||
}
|
||||
|
||||
pinHash, err := utils.HashPassword(req.NewPin)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to set PIN")
|
||||
}
|
||||
user.Password = pinHash
|
||||
if err := db.DB.Save(&user).Error; err != nil {
|
||||
return utils.Internal(c, "failed to set PIN")
|
||||
}
|
||||
|
||||
return issueMilerSession(c, cfg, user, req.DeviceToken)
|
||||
}
|
||||
}
|
||||
|
||||
func GetMilerProfile(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
|
||||
@@ -1119,7 +1182,9 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
return utils.NotFound(c, "assigned booking not found")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
// IST wall-clock (DBNow), so the compatibility-flow inwardedat stamped below
|
||||
// (and booking.updatedat) matches createdat and the reconciliation windows.
|
||||
now := utils.DBNow()
|
||||
booking.Status = constants.BookingPickedUp
|
||||
booking.Updatedat = now
|
||||
if err := tx.Save(&booking).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user