package utils import ( "crypto/rand" "errors" "math/big" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 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 } type Claims struct { UserID int `json:"userid"` Email string `json:"email"` RoleID int `json:"roleid"` TenantID int `json:"tenantid"` ConfigID int `json:"configid,omitempty"` HubID int `json:"hubid,omitempty"` jwt.RegisteredClaims } func GenerateToken(userID int, email string, roleID int, tenantID int, configID int, secret string) (string, error) { expirationTime := time.Now().Add(24 * time.Hour) claims := &Claims{ UserID: userID, Email: email, RoleID: roleID, TenantID: tenantID, ConfigID: configID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) } // GenerateHubStaffToken issues a JWT for hub console staff (role 6), carrying // HubID instead of TenantID/ConfigID so hub handlers can scope queries via // c.Locals("hubid") without colliding with the Miler role (5). func GenerateHubStaffToken(userID int, email string, hubID int, secret string) (string, error) { expirationTime := time.Now().Add(24 * time.Hour) claims := &Claims{ UserID: userID, Email: email, RoleID: 6, HubID: hubID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) } func ParseToken(tokenString string, secret string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { return []byte(secret), nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } return nil, errors.New("invalid token") } func IntPtr(v int) *int { return &v }