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:
@@ -1,7 +1,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -13,6 +15,55 @@ func HashPassword(password string) (string, error) {
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// dbLocation is the timezone the database records wall-clock timestamps in: the
|
||||
// connection DSN sets TimeZone=Asia/Kolkata, so CURRENT_TIMESTAMP column
|
||||
// defaults write IST wall-clock into timestamp-without-timezone columns.
|
||||
var dbLocation = func() *time.Location {
|
||||
if loc, err := time.LoadLocation("Asia/Kolkata"); err == nil {
|
||||
return loc
|
||||
}
|
||||
// A container without tzdata can't load the database; IST observes no DST,
|
||||
// so a fixed +05:30 offset is exact rather than an approximation.
|
||||
return time.FixedZone("IST", 5*3600+30*60)
|
||||
}()
|
||||
|
||||
// DBNow returns the current moment expressed as the wall-clock the database
|
||||
// stores, tagged UTC so the driver sends exactly those digits. Use it for any
|
||||
// comparison against a stored timestamp: comparing the container's UTC clock
|
||||
// against IST-stamped rows is what made date-range reports undercount.
|
||||
//
|
||||
// Deliberately independent of the container's own TZ, so it stays correct
|
||||
// whether or not TZ=Asia/Kolkata is set.
|
||||
func DBNow() time.Time {
|
||||
n := time.Now().In(dbLocation)
|
||||
return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), n.Minute(), n.Second(), n.Nanosecond(), time.UTC)
|
||||
}
|
||||
|
||||
// DBToday returns midnight at the start of the current database-local day.
|
||||
func DBToday() time.Time {
|
||||
n := DBNow()
|
||||
return time.Date(n.Year(), n.Month(), n.Day(), 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
// GenerateNumericOTP returns a random n-digit code, leading zeros preserved.
|
||||
// crypto/rand rather than math/rand: this is the only thing standing between a
|
||||
// parcel and someone claiming it was delivered, so a predictable sequence would
|
||||
// defeat the point.
|
||||
func GenerateNumericOTP(n int) string {
|
||||
const digits = "0123456789"
|
||||
out := make([]byte, n)
|
||||
for i := range out {
|
||||
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits))))
|
||||
if err != nil {
|
||||
// A failing system RNG must not silently downgrade to a guessable
|
||||
// code; the caller treats an empty OTP as "not issued".
|
||||
return ""
|
||||
}
|
||||
out[i] = digits[idx.Int64()]
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
|
||||
Reference in New Issue
Block a user