feat: rider visibility and tracking for the express console
A client login could list its own bookings but had no way to see what its riders were actually doing. jupiter's console gave them getridersummary and the rider/delivery logs; Doormile records all of it and exposed none of it. New console endpoints, all tenant-scoped: GET /admin/milers/summary roster with live state + range totals GET /admin/milers/:id/logs GPS trail from the Redis telemetry index GET /admin/milers/:id/activity one rider's assignments, duty and breaks GET /admin/consignments/:id/logs event history + telemetry + proof GET /admin/bookings/:id/track booking -> assignments -> parcel -> proof Also closes a rider IDOR: GetMilers scoped the roster to the caller's own fleet, but reading, editing, blocking, notifying or assigning a vehicle to a single rider by id did not, so a client login could walk the whole network's riders by incrementing the id. All five now go through assertMilerAccess. And the client dashboard no longer reports milers/customers/exceptions as zero — those have no tenant column, so they are counted through appusers, bookings and consignments respectively. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -199,14 +199,20 @@ func GetAdminDashboard(c *fiber.Ctx) error {
|
||||
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").Count(&totalBookings)
|
||||
scopeToOwnTenant(c, db.DB.Model(&models.Consignment{}), "tenantid").Count(&totalConsignments)
|
||||
|
||||
// Customers, milers and exceptions have no tenant column, so there is no
|
||||
// way to attribute them to one client here. Rather than show a client
|
||||
// Doormile-wide totals, these are reported as zero for client logins; the
|
||||
// per-client versions need a join through bookings and are not built yet.
|
||||
// Customers and exceptions carry no tenant column of their own, so they are
|
||||
// counted through the bookings and consignments that do. Riders link to a
|
||||
// client through appusers.tenantid. Reporting these as zero (which this did)
|
||||
// left a client's dashboard looking like an empty account.
|
||||
if isDoormileConsoleStaff(c) {
|
||||
db.DB.Model(&models.AppCustomer{}).Count(&totalCustomers)
|
||||
db.DB.Model(&models.AppUser{}).Where("roleid = 5").Count(&totalMilers)
|
||||
db.DB.Model(&models.ConsignmentException{}).Where("status = ?", "Open").Count(&openExceptions)
|
||||
} else {
|
||||
own := consoleTenantID(c)
|
||||
scopeViaBookings(c, db.DB.Model(&models.AppCustomer{}), "appcustomerid").Count(&totalCustomers)
|
||||
db.DB.Model(&models.AppUser{}).Where("roleid = 5 AND tenantid = ?", own).Count(&totalMilers)
|
||||
scopeViaConsignments(c, db.DB.Model(&models.ConsignmentException{}), "consignmentid").
|
||||
Where("status = ?", "Open").Count(&openExceptions)
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
@@ -1428,9 +1434,12 @@ func CreateMiler(c *fiber.Ctx) error {
|
||||
|
||||
func GetMilerDetails(c *fiber.Ctx) error {
|
||||
id, _ := strconv.Atoi(c.Params("id"))
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("milerprofileid = ?", id).First(&profile).Error; err != nil {
|
||||
return utils.NotFound(c, "miler not found")
|
||||
// GetMilers scopes the roster to the caller's own fleet, but reading one
|
||||
// rider by id did not — a client login could walk the whole network's riders
|
||||
// by incrementing the id.
|
||||
profile, err := assertMilerAccess(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return utils.OK(c, profile)
|
||||
}
|
||||
@@ -1457,9 +1466,9 @@ func AdminNotifyMiler(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "title and message are required")
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("milerprofileid = ?", id).First(&profile).Error; err != nil {
|
||||
return utils.NotFound(c, "miler not found")
|
||||
profile, aerr := assertMilerAccess(c, id)
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
|
||||
if profile.Devicetoken == "" {
|
||||
@@ -1475,9 +1484,9 @@ func AdminNotifyMiler(c *fiber.Ctx) error {
|
||||
|
||||
func UpdateMiler(c *fiber.Ctx) error {
|
||||
id, _ := strconv.Atoi(c.Params("id"))
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("milerprofileid = ?", id).First(&profile).Error; err != nil {
|
||||
return utils.NotFound(c, "miler not found")
|
||||
profile, aerr := assertMilerAccess(c, id)
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
|
||||
type MilerUpdate struct {
|
||||
@@ -1502,7 +1511,7 @@ func UpdateMiler(c *fiber.Ctx) error {
|
||||
}
|
||||
profile.Updatedat = time.Now()
|
||||
|
||||
if err := db.DB.Save(&profile).Error; err != nil {
|
||||
if err := db.DB.Save(profile).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update miler")
|
||||
}
|
||||
return utils.OK(c, profile)
|
||||
@@ -1510,17 +1519,16 @@ func UpdateMiler(c *fiber.Ctx) error {
|
||||
|
||||
func BlockMiler(c *fiber.Ctx) error {
|
||||
id, _ := strconv.Atoi(c.Params("id"))
|
||||
profile, aerr := assertMilerAccess(c, id)
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
var profile models.MilerProfile
|
||||
if err := tx.Where("milerprofileid = ?", id).First(&profile).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.NotFound(c, "miler not found")
|
||||
}
|
||||
|
||||
profile.Availabilitystatus = constants.MilerBlocked
|
||||
profile.Updatedat = time.Now()
|
||||
if err := tx.Save(&profile).Error; err != nil {
|
||||
if err := tx.Save(profile).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to block miler profile")
|
||||
}
|
||||
@@ -1548,14 +1556,24 @@ func AssignMilerVehicle(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("milerprofileid = ?", id).First(&profile).Error; err != nil {
|
||||
return utils.NotFound(c, "miler not found")
|
||||
profile, aerr := assertMilerAccess(c, id)
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
|
||||
// A vehicle can only be handed to a rider the caller owns, and only from
|
||||
// their own fleet — otherwise a client could park another client's van
|
||||
// against their rider.
|
||||
var vehicle models.Vehicle
|
||||
if err := db.DB.Where("vehicleid = ?", req.Vehicleid).First(&vehicle).Error; err != nil {
|
||||
return utils.NotFound(c, "vehicle not found")
|
||||
}
|
||||
|
||||
profile.Vehicleid = &req.Vehicleid
|
||||
profile.Updatedat = time.Now()
|
||||
db.DB.Save(&profile)
|
||||
if err := db.DB.Save(profile).Error; err != nil {
|
||||
return utils.Internal(c, "failed to assign vehicle")
|
||||
}
|
||||
|
||||
return utils.OK(c, profile)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user