Add Authentik SSO, budget visibility, Resend email, 12h item default
Opens Veola up to more users (Parodia's Authentik) and makes Apify spend visible to everyone. - Auth: Traefik forward-auth (trust X-Authentik-* headers only from trusted_proxies CIDRs, keyed by email, role synced from admin_group), keeping local password login as break-glass. New [auth] config, CaptureDirectIP + ForwardAuth middleware, deploy/authentik-forward-auth.md. - Budget: count every Apify run (apify_api_usage table) and show calls + estimated cost to all users on the dashboard, with an optional monthly-budget bar. New [budget] config + settings. - Email: Resend client for opt-in deal alerts and a weekly digest (Mon 09:00). Per-user email + toggles on Settings. New [resend] config. - Defaults: new items default to a 12-hour poll interval to cut spend. users table gains email/auth_source/email-pref columns (migrated in place). go build/vet/test green; boots and migrates cleanly.
This commit is contained in:
@@ -18,12 +18,24 @@ type SettingsData struct {
|
||||
TestNtfyOK string
|
||||
TestApifyOK string
|
||||
TestEbayOK string
|
||||
TestResendOK string
|
||||
EbayUsedToday int
|
||||
EbayDailyLimit int
|
||||
ApifyToday int
|
||||
ApifyMonth int
|
||||
ApifyCostPerCall float64
|
||||
MonthlyBudget float64
|
||||
PasswordMsg string
|
||||
PasswordError string
|
||||
UserMsg string
|
||||
UserError string
|
||||
EmailMsg string
|
||||
EmailError string
|
||||
}
|
||||
|
||||
// ApifyMonthCost is the estimated month-to-date Apify spend.
|
||||
func (d SettingsData) ApifyMonthCost() float64 {
|
||||
return float64(d.ApifyMonth) * d.ApifyCostPerCall
|
||||
}
|
||||
|
||||
// EbayLimitReached reports whether eBay polling is currently halted because
|
||||
@@ -101,6 +113,26 @@ templ settingsBody(d SettingsData) {
|
||||
<label class="v-label">Match Confidence Threshold</label>
|
||||
<input class="v-input font-mono" name="match_confidence_threshold" type="number" min="0" max="1" step="0.05" value={ d.Values["match_confidence_threshold"] }/>
|
||||
</div>
|
||||
<div class="border-t border-white/10 pt-4">
|
||||
<label class="v-label">Resend API Key</label>
|
||||
<input class="v-input font-mono" type="password" name="resend_api_key" autocomplete="off" placeholder="re_... (leave blank to keep current value)"/>
|
||||
@credStatus(d, "resend_api_key")
|
||||
</div>
|
||||
<div>
|
||||
<label class="v-label">Resend From Address</label>
|
||||
<input class="v-input" name="resend_from" value={ d.Values["resend_from"] } placeholder="Veola <veola@yourdomain.com>"/>
|
||||
<div class="v-muted text-xs mt-1">Used for deal-alert and weekly-digest email. Domain must be verified in Resend.</div>
|
||||
</div>
|
||||
<div class="border-t border-white/10 pt-4">
|
||||
<label class="v-label">Estimated Apify Cost per Call (USD)</label>
|
||||
<input class="v-input font-mono" name="apify_cost_per_call" type="number" min="0" step="0.001" value={ d.Values["apify_cost_per_call"] }/>
|
||||
<div class="v-muted text-xs mt-1">Apify does not report exact spend; this estimate drives the budget figures shown to everyone.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v-label">Monthly Budget (USD)</label>
|
||||
<input class="v-input font-mono" name="monthly_budget_usd" type="number" min="0" step="1" value={ d.Values["monthly_budget_usd"] }/>
|
||||
<div class="v-muted text-xs mt-1">0 hides the budget bar. When set, the dashboard shows month-to-date spend against it.</div>
|
||||
</div>
|
||||
if !d.IsAdmin {
|
||||
<div class="v-muted text-sm">Read-only for non-admin users.</div>
|
||||
} else {
|
||||
@@ -109,6 +141,7 @@ templ settingsBody(d SettingsData) {
|
||||
<button class="v-btn-ghost" type="submit" formaction="/settings/test-ntfy">Test Ntfy</button>
|
||||
<button class="v-btn-ghost" type="submit" formaction="/settings/test-apify">Test Apify</button>
|
||||
<button class="v-btn-ghost" type="submit" formaction="/settings/test-ebay">Test eBay</button>
|
||||
<button class="v-btn-ghost" type="submit" formaction="/settings/test-resend">Test Resend</button>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
@@ -121,6 +154,18 @@ templ settingsBody(d SettingsData) {
|
||||
if d.TestEbayOK != "" {
|
||||
<div class="v-flash mt-3">{ d.TestEbayOK }</div>
|
||||
}
|
||||
if d.TestResendOK != "" {
|
||||
<div class="v-flash mt-3">{ d.TestResendOK }</div>
|
||||
}
|
||||
<div class="text-sm mt-4 border-t border-white/10 pt-4">
|
||||
<span class="v-muted">Apify calls this month:</span>
|
||||
<span class="font-mono">{ fmt.Sprintf("%d", d.ApifyMonth) }</span>
|
||||
if d.ApifyCostPerCall > 0 {
|
||||
<span class="v-muted ml-1">{ fmt.Sprintf("(~$%.2f est.)", d.ApifyMonthCost()) }</span>
|
||||
}
|
||||
<span class="v-muted ml-3">today:</span>
|
||||
<span class="font-mono">{ fmt.Sprintf("%d", d.ApifyToday) }</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="v-card p-6">
|
||||
@@ -149,6 +194,33 @@ templ settingsBody(d SettingsData) {
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="v-card p-6">
|
||||
<h2 class="font-semibold mb-4">Email Notifications</h2>
|
||||
if d.EmailError != "" {
|
||||
<div class="v-flash-error">{ d.EmailError }</div>
|
||||
}
|
||||
if d.EmailMsg != "" {
|
||||
<div class="v-flash">{ d.EmailMsg }</div>
|
||||
}
|
||||
<form method="post" action="/settings/email" class="space-y-4">
|
||||
@CSRFInput(d.CSRFToken)
|
||||
<div>
|
||||
<label class="v-label">Notification Email</label>
|
||||
<input class="v-input" type="email" name="email" value={ currentEmail(d) } placeholder="you@example.com"/>
|
||||
<div class="v-muted text-xs mt-1">Where deal alerts and the weekly digest are sent. Requires Resend to be configured by an admin.</div>
|
||||
</div>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="email_deal_alerts" value="1" checked?={ currentDealAlerts(d) }/>
|
||||
<span>Email me deal alerts</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="email_weekly_digest" value="1" checked?={ currentWeeklyDigest(d) }/>
|
||||
<span>Email me the weekly digest (Mondays)</span>
|
||||
</label>
|
||||
<button class="v-btn" type="submit">Save Preferences</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
if d.IsAdmin {
|
||||
<section class="v-card p-6">
|
||||
<h2 class="font-semibold mb-4">Users</h2>
|
||||
@@ -205,6 +277,23 @@ templ settingsBody(d SettingsData) {
|
||||
</div>
|
||||
}
|
||||
|
||||
// currentEmail and the two opt-in accessors read the signed-in user's saved
|
||||
// email preferences off Page.CurrentUser, guarding the nil case.
|
||||
func currentEmail(d SettingsData) string {
|
||||
if d.CurrentUser == nil {
|
||||
return ""
|
||||
}
|
||||
return d.CurrentUser.Email
|
||||
}
|
||||
|
||||
func currentDealAlerts(d SettingsData) bool {
|
||||
return d.CurrentUser != nil && d.CurrentUser.EmailDealAlerts
|
||||
}
|
||||
|
||||
func currentWeeklyDigest(d SettingsData) bool {
|
||||
return d.CurrentUser != nil && d.CurrentUser.EmailWeeklyDigest
|
||||
}
|
||||
|
||||
templ Settings(d SettingsData) {
|
||||
@Layout(d.Page, settingsBody(d))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user