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

@@ -28,26 +28,31 @@ func (s *Scheduler) emailClient(ctx context.Context) *email.Client {
return email.New(apiKey, from)
}
// sendDealEmails mails a deal alert to the item's owner, if that user opted
// into deal-alert email and has a destination address. Items are private, so a
// deal only ever notifies the person who is watching it. Entirely best-effort:
// an unconfigured Resend account or a send failure is logged and swallowed.
func (s *Scheduler) sendDealEmails(ctx context.Context, it models.Item, r apify.UnifiedResult) {
// dealMailer resolves the item owner and Resend client for deal-alert email.
// It returns (nil, nil) when email should be skipped (Resend unconfigured, no
// owner, owner opted out, or no address) so the caller can resolve once per
// poll instead of once per result. Best-effort: errors are logged and swallowed.
func (s *Scheduler) dealMailer(ctx context.Context, it models.Item) (*email.Client, *models.User) {
client := s.emailClient(ctx)
if !client.Configured() {
return
}
if it.UserID == 0 {
return
if !client.Configured() || it.UserID == 0 {
return nil, nil
}
owner, err := s.store.GetUserByID(ctx, it.UserID)
if err != nil {
slog.Error("load item owner failed", "item_id", it.ID, "err", err)
return
return nil, nil
}
if owner == nil || !owner.EmailDealAlerts || owner.Email == "" {
return
return nil, nil
}
return client, owner
}
// sendDealEmail mails a single deal alert to the resolved owner. The client and
// owner come from dealMailer (resolved once per poll), so this does no DB or
// settings I/O per result. Items are private, so a deal only ever notifies the
// person watching it.
func (s *Scheduler) sendDealEmail(ctx context.Context, client *email.Client, owner *models.User, it models.Item, r apify.UnifiedResult) {
subject := fmt.Sprintf("Veola deal: %s", it.Name)
price := fmt.Sprintf("%s%.2f", currencyPrefix(r.Currency), r.Price)
target := ""