A send that the mail provider did not accept finished its background
transaction as ok, so rejected reset emails looked delivered in tracing.
It now finishes as an error with a fixed message that names no recipient.
The channel used DropWrite, under which TryWrite reports success and
discards the email, so a full queue still counted the request and never
sent the code. Wait makes TryWrite return false when the queue is full,
without blocking, so the request is released and the user can ask again.
The legacy ingest batch holds the per-work-order lock, taken in id order,
for every work order it may cancel until the batch commits. A vendor
uplift request now runs under the same lock. Uplift creation on both
routes refuses a work order cancelled by either lifecycle or status text,
so a request can neither slip past a cancel nor land after one.
A token carries the user's roles and account, so a demoted admin kept admin
claims until the token expired. The team member update and the admin user
edit now rotate the security stamp and evict the cached value when the role,
account or user name changes. Permission overrides are read per request and
are not in the token.
Tokens now carry a keyed hash of the account's security stamp, and every
authenticated request compares it with the stored stamp (cached for 60 s,
evicted in-process on change). A password reset or change, a deactivation
and a deletion all rotate or remove the stamp, so tokens issued before them
get 401. Tokens without the claim get 401 too.
An admin revoke overturns a human decision, so admins may revoke only
admin-approved uplifts. The work-order revoke path let an admin revoke an
auto-approved uplift they had requested themselves. Both revoke endpoints
now refuse it; dispatchers keep revoking their own auto-approved uplifts.
- Forgot Password is limited to 3 codes an hour and 10 a day per email, and
an account gets 10 failed code checks a day across every code it is sent,
so new client addresses and new codes no longer buy more guesses. Refused
requests answer exactly like accepted ones.
- The reset email is queued to a background sender, and unregistered
addresses store a row no code can match, so both paths do the same work
and return without waiting on the mail provider. Each request also clears
expired codes.
- Code hashes are HMAC-SHA256 under a key derived with HKDF from the JWT
signing secret; rows in the previous unkeyed format stop matching.
- Email and code are read only from the JSON body.
Two concurrent first requests can leave two pending codes for one email.
Exhausting or using one now deletes all of them, so a sibling code cannot
become live afterwards.
Forgot Password answers every address the same way and emails a code only
to an active account. Codes are stored as salted SHA-256 hashes, expire 15
minutes after issue, are replaced by a newer request, and are checked only
against the email they were issued to. Five failed checks delete the code;
attempts are reserved with one conditional UPDATE so concurrent guesses
cannot exceed the budget. VerificationCode requires the email, and email and
code are accepted in the JSON body so they stay out of URLs.
The three anonymous endpoints are rate limited to 10 requests per 15
minutes per client IP. Forwarded headers are trusted only through loopback
and private hops, since the API sits behind the EB load balancer and nginx.
The migration adds hash, salt, expiry and attempt columns and deletes the
old plaintext rows.
User creation, contact, calendar event and site-contact create/modify/delete stamps used local server time. Validation rules comparing user-entered dates and the JWT expiry are unchanged.
WorkOrderDataService.AddAsync overwrote the UTC CreatedDate set by WorkOrderService with local server time, offsetting the SLA response clock on any host not running in UTC. Data services now stamp CreatedDate, LastModificationTime and DeletionTime with DateTime.UtcNow, and a work order keeps the creation time its caller set.
GET api/team-members/me/permissions returns the keys the signed-in user
holds after role defaults and their own overrides, evaluated by the same
policy that guards writes. The user comes from the token; a missing or
unknown identity gets 401.
- Legacy reads (by id, all, by client, paged, address book, exists, count) and the vendor
preference site check now skip tombstoned sites
- Create requires a client, street address, city, state and at least one complete contact
- Reject duplicate site codes per client (case-insensitive); site code is immutable once set
- Delete tombstones the site and requires the DeleteSites permission (Admin, Scheduler)
- GET /api/locations/{id}/open-work-orders returns the open count and ids
- PATCH /api/locations/{id}/contact-info saves contacts and notes from the work-order Site dialog
- Add nullable Locations.Notes, used as the site-level POC notes fallback
Reactive/Emergency work orders with a SEV 1-5 level are timed from their
creation against the SEV Respond deadline (2/4/8/24/72 hours, one backend
table). From 50% they are at risk: a dismissable High row in the "SLA at
Risk" section and an entry in the feed's slaAtRisk set with the server
clock (start, deadline, percent) for the banner and toast. From 100% they
are a Critical acknowledge row that only acknowledging removes.
POST /api/notifications/sla/{id}/acknowledge records who and when as a
work-order audit entry ("SLA breach acknowledged by <name>"), scoped to the
caller's feed audience: 404 outside it, 409 before the deadline, 204 when
recorded or already recorded. A later severity change is a new breach.