Compare commits
2 Commits
e714e1de73
...
29e189b7e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 29e189b7e0 | |||
| f1dbf7edc9 |
@@ -362,6 +362,162 @@ func CxSignup(cfg *config.Config) fiber.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── PIN auth (interim — until the OTP/SMS gateway is live) ───────────────────
|
||||||
|
//
|
||||||
|
// Mirrors the miler login flow (LoginMiler / SetMilerPin / VerifyMilerPin) for
|
||||||
|
// customers: a first-time customer sets their own PIN, a returning one enters
|
||||||
|
// it. This exists because no SMS/OTP provider is plugged in yet (see
|
||||||
|
// internal/sms). The OTP endpoints above stay in place — the app can switch back
|
||||||
|
// to them the moment a gateway is live.
|
||||||
|
|
||||||
|
// validCxPin accepts exactly a 4-digit PIN, matching the app's keypad.
|
||||||
|
func validCxPin(pin string) bool {
|
||||||
|
pin = strings.TrimSpace(pin)
|
||||||
|
if len(pin) != cxOtpLength {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range pin {
|
||||||
|
if r < '0' || r > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CxPinLogin resolves a phone number to the screen the app shows next: register
|
||||||
|
// (no account), set-PIN (account, no PIN yet) or enter-PIN (account with a PIN).
|
||||||
|
func CxPinLogin(c *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return utils.CxBadRequest(c, "We could not read that request")
|
||||||
|
}
|
||||||
|
phone, _, ok := normalizePhone(req.Phone)
|
||||||
|
if !ok {
|
||||||
|
return utils.CxBadRequest(c, "Enter a valid phone number")
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer models.AppCustomer
|
||||||
|
if db.DB.Where("phone = ?", phone).First(&customer).Error != nil {
|
||||||
|
// No account yet — the app collects a name + a new PIN on the next screen.
|
||||||
|
return utils.CxOK(c, fiber.Map{"phone": phone, "registered": false, "pin_set": false})
|
||||||
|
}
|
||||||
|
if customer.Status == constants.CustomerStatusBlocked {
|
||||||
|
return utils.CxForbidden(c, "You do not have access to this")
|
||||||
|
}
|
||||||
|
return utils.CxOK(c, fiber.Map{
|
||||||
|
"phone": phone,
|
||||||
|
"registered": true,
|
||||||
|
"pin_set": customer.Loginpinhash != "",
|
||||||
|
"name": strings.TrimSpace(customer.Firstname + " " + customer.Lastname),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CxSetPin creates a customer's PIN the first time — signing up a brand-new
|
||||||
|
// phone (name required) or setting the first PIN on an account that has none. It
|
||||||
|
// refuses to overwrite an existing PIN (409), so the phone-only unauthenticated
|
||||||
|
// path here cannot take over an account that already has one. On success the
|
||||||
|
// customer is logged in immediately.
|
||||||
|
func CxSetPin(cfg *config.Config) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
NewPin string `json:"new_pin"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return utils.CxBadRequest(c, "We could not read that request")
|
||||||
|
}
|
||||||
|
phone, _, ok := normalizePhone(req.Phone)
|
||||||
|
if !ok {
|
||||||
|
return utils.CxBadRequest(c, "Enter a valid phone number")
|
||||||
|
}
|
||||||
|
if !validCxPin(req.NewPin) {
|
||||||
|
return utils.CxBadRequest(c, "Your PIN must be 4 digits")
|
||||||
|
}
|
||||||
|
pinHash, herr := utils.HashPassword(req.NewPin)
|
||||||
|
if herr != nil {
|
||||||
|
utils.Error("CxSetPin: could not hash PIN", "error", herr)
|
||||||
|
return utils.CxInternal(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer models.AppCustomer
|
||||||
|
if db.DB.Where("phone = ?", phone).First(&customer).Error != nil {
|
||||||
|
// New account: a name is required, same as signup.
|
||||||
|
first, last := splitName(req.Name)
|
||||||
|
if first == "" {
|
||||||
|
return utils.CxFail(c, fiber.StatusBadRequest, utils.CxErrInvalidName, "Enter your full name")
|
||||||
|
}
|
||||||
|
customer = models.AppCustomer{
|
||||||
|
Firstname: first,
|
||||||
|
Lastname: last,
|
||||||
|
Phone: phone,
|
||||||
|
Loginpinhash: pinHash,
|
||||||
|
Status: constants.CustomerStatusActive,
|
||||||
|
Configid: cxDefaultConfig,
|
||||||
|
}
|
||||||
|
if cerr := db.DB.Create(&customer).Error; cerr != nil {
|
||||||
|
utils.Error("CxSetPin: could not create customer", "error", cerr)
|
||||||
|
return utils.CxInternal(c)
|
||||||
|
}
|
||||||
|
return issueCxSession(c, cfg, &customer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if customer.Status == constants.CustomerStatusBlocked {
|
||||||
|
return utils.CxForbidden(c, "You do not have access to this")
|
||||||
|
}
|
||||||
|
if customer.Loginpinhash != "" {
|
||||||
|
return utils.CxFail(c, fiber.StatusConflict, utils.CxErrPinAlreadySet, "A PIN is already set — enter it to sign in")
|
||||||
|
}
|
||||||
|
customer.Loginpinhash = pinHash
|
||||||
|
if err := db.DB.Save(&customer).Error; err != nil {
|
||||||
|
utils.Error("CxSetPin: could not set PIN", "error", err)
|
||||||
|
return utils.CxInternal(c)
|
||||||
|
}
|
||||||
|
return issueCxSession(c, cfg, &customer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CxVerifyPin signs a returning customer in with their PIN.
|
||||||
|
func CxVerifyPin(cfg *config.Config) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Pin string `json:"pin"`
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return utils.CxBadRequest(c, "We could not read that request")
|
||||||
|
}
|
||||||
|
phone, _, ok := normalizePhone(req.Phone)
|
||||||
|
if !ok || strings.TrimSpace(req.Pin) == "" {
|
||||||
|
return utils.CxBadRequest(c, "Enter your PIN")
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer models.AppCustomer
|
||||||
|
if db.DB.Where("phone = ?", phone).First(&customer).Error != nil {
|
||||||
|
// Same generic message for unknown phone and wrong PIN, so this can't
|
||||||
|
// be used to enumerate who is registered.
|
||||||
|
return utils.CxFail(c, fiber.StatusUnauthorized, utils.CxErrInvalidPin, "That phone number or PIN is incorrect")
|
||||||
|
}
|
||||||
|
if customer.Status == constants.CustomerStatusBlocked {
|
||||||
|
return utils.CxForbidden(c, "You do not have access to this")
|
||||||
|
}
|
||||||
|
if customer.Loginpinhash == "" {
|
||||||
|
return utils.CxFail(c, fiber.StatusConflict, utils.CxErrPinNotSet, "No PIN set yet — create one to continue")
|
||||||
|
}
|
||||||
|
if !utils.CheckPasswordHash(strings.TrimSpace(req.Pin), customer.Loginpinhash) {
|
||||||
|
return utils.CxFail(c, fiber.StatusUnauthorized, utils.CxErrInvalidPin, "That phone number or PIN is incorrect")
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
customer.Lastloginat = &now
|
||||||
|
if err := db.DB.Save(&customer).Error; err != nil {
|
||||||
|
utils.Warn("CxVerifyPin: could not stamp last login", "error", err)
|
||||||
|
}
|
||||||
|
return issueCxSession(c, cfg, &customer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CxVerifyOtp exchanges a code for a session.
|
// CxVerifyOtp exchanges a code for a session.
|
||||||
func CxVerifyOtp(cfg *config.Config) fiber.Handler {
|
func CxVerifyOtp(cfg *config.Config) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
|
|||||||
@@ -114,6 +114,14 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
|||||||
customer.Post("/auth/otp/verify", authThrottle, middlewares.Idempotency(), controllers.CxVerifyOtp(cfg))
|
customer.Post("/auth/otp/verify", authThrottle, middlewares.Idempotency(), controllers.CxVerifyOtp(cfg))
|
||||||
customer.Post("/auth/refresh", authThrottle, controllers.CxRefresh(cfg))
|
customer.Post("/auth/refresh", authThrottle, controllers.CxRefresh(cfg))
|
||||||
|
|
||||||
|
// Interim PIN auth, until the SMS/OTP gateway is live (see internal/sms).
|
||||||
|
// Same shape as the miler flow: /login reports whether a PIN is set, then the
|
||||||
|
// app routes to /set-pin (first time / new account) or /verify-pin (returning).
|
||||||
|
// set-pin can't overwrite an existing PIN, so it's safe unauthenticated here.
|
||||||
|
customer.Post("/auth/login", authThrottle, controllers.CxPinLogin)
|
||||||
|
customer.Post("/auth/set-pin", authThrottle, controllers.CxSetPin(cfg))
|
||||||
|
customer.Post("/auth/verify-pin", authThrottle, controllers.CxVerifyPin(cfg))
|
||||||
|
|
||||||
// Serviceability and configuration are read before sign-in: the booking
|
// Serviceability and configuration are read before sign-in: the booking
|
||||||
// form is explorable without an account, and gating the state picker behind
|
// form is explorable without an account, and gating the state picker behind
|
||||||
// auth would make the app's first screen a login wall.
|
// auth would make the app's first screen a login wall.
|
||||||
|
|||||||
70
scratch/seed_pin_customer.go
Normal file
70
scratch/seed_pin_customer.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
// seed_pin_customer.go — one ready-to-use customer for testing the interim PIN
|
||||||
|
// login flow: phone +919000001234, PIN 1234, already set. Idempotent (re-running
|
||||||
|
// just re-asserts the PIN to 1234).
|
||||||
|
//
|
||||||
|
// The phone is stored in NORMALISED +91 form, because CxPinLogin/CxVerifyPin
|
||||||
|
// normalise the entered number to +91 before the DB lookup — a bare 10-digit
|
||||||
|
// row would never be found.
|
||||||
|
//
|
||||||
|
// Run: go run scratch/seed_pin_customer.go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"doormile/config"
|
||||||
|
"doormile/db"
|
||||||
|
"doormile/models"
|
||||||
|
"doormile/utils"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
_ = godotenv.Load()
|
||||||
|
cfg := config.Load()
|
||||||
|
db.Connect(cfg)
|
||||||
|
if db.DB == nil {
|
||||||
|
log.Fatal("DB connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
const phone = "+919000001234" // customer types 9000001234; it normalises to this
|
||||||
|
pinHash, err := utils.HashPassword("1234")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("hash: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var c models.AppCustomer
|
||||||
|
if db.DB.Where("phone = ?", phone).First(&c).Error != nil {
|
||||||
|
c = models.AppCustomer{
|
||||||
|
Firstname: "Test",
|
||||||
|
Lastname: "PinCustomer",
|
||||||
|
Phone: phone,
|
||||||
|
Email: "test.pin@doormile.in",
|
||||||
|
Loginpinhash: pinHash,
|
||||||
|
Defaultlatitude: 11.0168,
|
||||||
|
Defaultlongitude: 76.9558,
|
||||||
|
Defaultpincode: "641012",
|
||||||
|
Status: "Active",
|
||||||
|
Configid: 1001,
|
||||||
|
}
|
||||||
|
if err := db.DB.Create(&c).Error; err != nil {
|
||||||
|
log.Fatalf("create: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("created customer with PIN 1234")
|
||||||
|
} else {
|
||||||
|
c.Loginpinhash = pinHash
|
||||||
|
c.Status = "Active"
|
||||||
|
if err := db.DB.Save(&c).Error; err != nil {
|
||||||
|
log.Fatalf("update: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("customer already existed — PIN re-asserted to 1234")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n✅ ready: appcustomerid=%d phone=%s login-as=9000001234 pin=1234\n",
|
||||||
|
c.Appcustomerid, c.Phone)
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ const (
|
|||||||
CxErrInvalid = "invalid"
|
CxErrInvalid = "invalid"
|
||||||
CxErrInvalidName = "invalid_name"
|
CxErrInvalidName = "invalid_name"
|
||||||
CxErrInvalidOtp = "invalid_otp"
|
CxErrInvalidOtp = "invalid_otp"
|
||||||
|
CxErrInvalidPin = "invalid_pin"
|
||||||
|
CxErrPinNotSet = "pin_not_set"
|
||||||
|
CxErrPinAlreadySet = "pin_already_set"
|
||||||
CxErrUnauthorized = "unauthorized"
|
CxErrUnauthorized = "unauthorized"
|
||||||
CxErrForbidden = "forbidden"
|
CxErrForbidden = "forbidden"
|
||||||
CxErrNotFound = "not_found"
|
CxErrNotFound = "not_found"
|
||||||
|
|||||||
Reference in New Issue
Block a user