Harden forward-auth, per-user isolation, and budget accuracy

Code-review fixes on the Authentik/budget/Resend work:

- UpsertForwardUser: avoid 500 lockout on username UNIQUE collision and
  on the concurrent first-login race; normalize emails (lower+trim) so
  case variants don't create duplicate accounts.
- Centralize email management in Authentik: when forward-auth mode is on,
  email is read-only in Veola for every user (not just forward rows);
  pure-local deployments keep editable email for Resend.
- Fail closed on per-user isolation: ownedItem rejects uid==0/orphaned
  items; dashboard and global results error on a userless request.
- Budget accuracy: count Apify usage only after a successful run, and
  count the billed Test Apify run.
- Efficiency: resolve the deal-email recipient once per poll; build the
  settings page once in PostEmailPrefs.
This commit is contained in:
prosolis
2026-06-20 14:05:04 -07:00
parent feea126c5e
commit 4fb1a4553e
10 changed files with 174 additions and 78 deletions

View File

@@ -221,6 +221,10 @@ func (s *Scheduler) RunPoll(ctx context.Context, it models.Item) {
)
bestIdx := PickBest(results)
// Resolve the deal-email recipient once: owner and Resend client are
// constant for the whole poll, so per-result sends reuse them instead of
// re-reading settings and the owner row for every alerting result.
dealClient, dealOwner := s.dealMailer(ctx, it)
alertsSent := 0
for _, r := range results {
exists, err := s.store.ResultExists(ctx, it.ID, r.URL)
@@ -247,9 +251,11 @@ func (s *Scheduler) RunPoll(ctx context.Context, it models.Item) {
alerted = true
alertsSent++
}
// Email opted-in users in addition to ntfy. Best-effort: a mail
// failure must not block result storage or the next alert.
s.sendDealEmails(ctx, it, r)
// Email the owner in addition to ntfy. Best-effort: a mail failure
// must not block result storage or the next alert.
if dealOwner != nil {
s.sendDealEmail(ctx, dealClient, dealOwner, it, r)
}
}
price := r.Price
_, err = s.store.InsertResult(ctx, &models.Result{
@@ -323,10 +329,17 @@ func (s *Scheduler) apifyClient(ctx context.Context) *apify.Client {
// usage counter that feeds the budget view. Every Apify-billed run in Veola
// goes through here so the count stays honest.
func (s *Scheduler) runApifyActor(ctx context.Context, actorID string, input any) ([]json.RawMessage, error) {
raw, err := s.apifyClient(ctx).Run(ctx, actorID, input)
if err != nil {
// Count only runs that actually reached Apify. Counting before the call
// would inflate the budget estimate with config errors and failed runs
// that Apify never billed.
return nil, err
}
if err := s.store.IncrementApifyUsage(ctx); err != nil {
slog.Error("apify usage increment failed", "err", err)
}
return s.apifyClient(ctx).Run(ctx, actorID, input)
return raw, nil
}
// ApifyUsage reports Apify run counts and the cost model used by the budget