fix: console tenant scoping, miler identity spoofing, delivery proof, timezone
Security - Express console had no tenant scoping at all: LoginAdmin hardcoded tenantid 0 into every JWT and none of the 85 admin handlers filtered by tenant, so any client given a console login would read every other client's bookings, customers, pricing and reports. Adds DoormileAuth.Tenantid (nil = Doormile staff, unrestricted; set = client, scoped), emits it in the token, and scopes reads, guards writes and pins tenantid on create. - Miler telemetry (/miler/logs, /miler/status, /miler/consignments/logs) took userid from the request body, letting any authenticated rider write another rider's status and GPS trail — data the dispatch layer reasons over. Identity now comes from the token. - POST /miler/reset-pin was unauthenticated and overwrote a PIN given only a phone number, so reset-pin + verify-pin took over any rider account. Now requires admin/manager/executive auth. Correctness - Date ranges compared the container's UTC clock against timestamps the DB writes as IST wall-clock (DSN sets TimeZone=Asia/Kolkata), so "today so far" ended 5h30m in the past and silently dropped everything created after noon IST from every report. Sets TZ in the image and adds utils.DBNow/DBToday, which stay correct regardless of container timezone. - CreateMiler never set Configid, so console-created riders got the column default of 1 while LoginMiler looks up configid 1001 — every such rider was unable to log in, reported as "no miler account found". - Delivery wrote no consignment history row, so a tracking timeline never showed the parcel arriving. Features - Delivery OTP is now real (crypto/rand, issued to the receiver, verified and cleared on delivery) but opt-in per client via Tenant.Requiredeliveryotp, defaulting off — friction worth it for a courier parcel, not a food order. - Express bookings accept pickuplocationid, so the console can name a client site (a DailyGrubs kitchen) instead of retyping its address; validated against the tenant and carried through to the consignment. - TenantLocation.Locationname, miler tenantid/hubid, Nagercoil (629) opened. - PUT /miler/availability accepts both "status" and "availabilitystatus", and /miler/location no longer drops speed/heading — both were contract mismatches against the doc the Flutter dev was given. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -293,7 +293,8 @@ func UpdateMilerAvailability(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Status == "" {
|
||||
status := req.ResolvedStatus()
|
||||
if status == "" {
|
||||
return utils.BadRequest(c, "status is required")
|
||||
}
|
||||
|
||||
@@ -302,7 +303,7 @@ func UpdateMilerAvailability(c *fiber.Ctx) error {
|
||||
return utils.NotFound(c, "miler profile not found")
|
||||
}
|
||||
|
||||
profile.Availabilitystatus = req.Status
|
||||
profile.Availabilitystatus = status
|
||||
profile.Updatedat = time.Now()
|
||||
if err := db.DB.Save(&profile).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update availability")
|
||||
@@ -796,9 +797,13 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
consignmentTenantID = *booking.Tenantid
|
||||
}
|
||||
|
||||
// Carried over so the consignment stays traceable to the client site it was
|
||||
// collected from — for a food client that's the kitchen, and "how many
|
||||
// parcels went out of which kitchen" is unanswerable without it.
|
||||
consignment := models.Consignment{
|
||||
Trackingno: trackingNo,
|
||||
Tenantid: consignmentTenantID,
|
||||
Pickuplocationid: booking.Pickuplocationid,
|
||||
Pickuplatitude: booking.Pickuplatitude,
|
||||
Pickuplongitude: booking.Pickuplongitude,
|
||||
Deliverylatitude: booking.Deliverylatitude,
|
||||
@@ -829,6 +834,17 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// A hyperlocal parcel goes straight out for delivery, so its receiver OTP has
|
||||
// to exist before this transaction commits. Anything routed via a hub gets
|
||||
// its OTP when it actually leaves for the final mile instead. Only issued
|
||||
// for clients that ask for it — see Tenant.Requiredeliveryotp.
|
||||
if consignmentStatus == constants.ConsignmentOutForDelivery {
|
||||
var tenant models.Tenant
|
||||
if tx.Where("tenantid = ?", consignmentTenantID).First(&tenant).Error == nil && tenant.Requiredeliveryotp {
|
||||
consignment.Deliveryotp = utils.GenerateNumericOTP(6)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Create(&consignment).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to convert booking to consignment")
|
||||
@@ -865,15 +881,18 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
||||
if notifyErr := notify.SendToDevice(
|
||||
customer.Devicetoken,
|
||||
"Parcel Picked Up",
|
||||
fmt.Sprintf("Parcel picked up — Tracking No: %s", trackingNo),
|
||||
map[string]string{
|
||||
"booking_id": strconv.Itoa(bookingID),
|
||||
"tracking_no": trackingNo,
|
||||
},
|
||||
); notifyErr != nil {
|
||||
body := fmt.Sprintf("Parcel picked up — Tracking No: %s", trackingNo)
|
||||
payload := map[string]string{
|
||||
"booking_id": strconv.Itoa(bookingID),
|
||||
"tracking_no": trackingNo,
|
||||
}
|
||||
// The OTP goes to the receiver and only the receiver — the rider has to
|
||||
// be told it at the door, which is what makes it proof of handover.
|
||||
if consignment.Deliveryotp != "" {
|
||||
body = fmt.Sprintf("%s. Share OTP %s with the rider on delivery.", body, consignment.Deliveryotp)
|
||||
payload["delivery_otp"] = consignment.Deliveryotp
|
||||
}
|
||||
if notifyErr := notify.SendToDevice(customer.Devicetoken, "Parcel Picked Up", body, payload); notifyErr != nil {
|
||||
utils.Warn("FCM: failed to notify customer on pickup", "booking_id", bookingID, "error", notifyErr)
|
||||
}
|
||||
}
|
||||
@@ -919,6 +938,11 @@ func CreateMilerPeriodicLog(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
// The rider's identity comes from their token, never the body. Trusting a
|
||||
// client-supplied userid let any authenticated miler write another miler's
|
||||
// GPS trail, which feeds the location data dispatch reasons over.
|
||||
log.UserID = c.Locals("userid").(int)
|
||||
|
||||
t, err := time.Parse("2006-01-02 15:04:05", log.LogDate)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid logdate format — expected YYYY-MM-DD HH:MM:SS")
|
||||
@@ -987,8 +1011,12 @@ func CreateMilerStatus(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if status.UserID == 0 || status.Status == "" {
|
||||
return utils.BadRequest(c, "userid and status are required")
|
||||
// Identity from the token, not the body — otherwise one rider can set
|
||||
// another rider's live status.
|
||||
status.UserID = c.Locals("userid").(int)
|
||||
|
||||
if status.Status == "" {
|
||||
return utils.BadRequest(c, "status is required")
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("miler_status:%d", status.UserID)
|
||||
@@ -1088,10 +1116,16 @@ func PublishConsignmentLogs(c *fiber.Ctx) error {
|
||||
return utils.Internal(c, "cache service unavailable")
|
||||
}
|
||||
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
|
||||
pipe := db.Rdb.TxPipeline()
|
||||
tx := db.DB.Begin()
|
||||
|
||||
for _, item := range input {
|
||||
// Same rule as the other telemetry writers: the token owns the identity,
|
||||
// so a batch can't be attributed to some other rider.
|
||||
item.UserID = milerUserID
|
||||
|
||||
logTime, err := time.Parse("2006-01-02 15:04:05", item.LogDate)
|
||||
if err != nil {
|
||||
logTime = time.Now()
|
||||
|
||||
Reference in New Issue
Block a user