Make Resend from-address config-only; drop credential status rows
The Settings page no longer renders the Resend From Address input or any of the read-only "Set in config.toml" credential status rows (Apify key, eBay id/secret, ntfy auth, Resend key). Those rows looked like editable inputs but were dead text, and duplicated what the Test buttons verify more authoritatively. - resend_from is now sourced only from config.toml ([resend] from); it is removed from settingsKeys, the test-Resend handler, the scheduler email client, and the schema seed. - Removed the credStatus templ component, the CredentialStatus field on SettingsData, and the credentialStatus() handler helper.
This commit is contained in:
@@ -43,9 +43,9 @@ func (c AuthConfig) ForwardAuthEnabled() bool {
|
|||||||
return strings.EqualFold(strings.TrimSpace(c.Mode), "forward")
|
return strings.EqualFold(strings.TrimSpace(c.Mode), "forward")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResendConfig holds Resend transactional-email credentials. APIKey and From
|
// ResendConfig holds Resend transactional-email credentials, set only in
|
||||||
// can both be overridden at runtime via /settings (resend_api_key,
|
// config.toml (never via the web UI). From is an RFC-5322 address, e.g.
|
||||||
// resend_from). From is an RFC-5322 address, e.g. "Veola <veola@example.com>".
|
// "Veola <veola@example.com>".
|
||||||
type ResendConfig struct {
|
type ResendConfig struct {
|
||||||
APIKey string `toml:"api_key"`
|
APIKey string `toml:"api_key"`
|
||||||
From string `toml:"from"`
|
From string `toml:"from"`
|
||||||
|
|||||||
@@ -116,8 +116,7 @@ INSERT OR IGNORE INTO settings (key, value) VALUES
|
|||||||
('match_confidence_threshold', '0.6'),
|
('match_confidence_threshold', '0.6'),
|
||||||
('apify_cost_per_call', '0.00'),
|
('apify_cost_per_call', '0.00'),
|
||||||
('monthly_budget_usd', '0.00'),
|
('monthly_budget_usd', '0.00'),
|
||||||
('resend_api_key', ''),
|
('resend_api_key', '');
|
||||||
('resend_from', '');
|
|
||||||
|
|
||||||
-- apify_api_usage tracks Apify actor runs per UTC day so the operator (and
|
-- apify_api_usage tracks Apify actor runs per UTC day so the operator (and
|
||||||
-- every signed-in user) can see consumption and an estimated spend. Apify
|
-- every signed-in user) can see consumption and an estimated spend. Apify
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import (
|
|||||||
// settingsKeys are the non-secret operational settings editable from the
|
// settingsKeys are the non-secret operational settings editable from the
|
||||||
// Settings page. API credentials (Apify, eBay, ntfy, Resend) are deliberately
|
// Settings page. API credentials (Apify, eBay, ntfy, Resend) are deliberately
|
||||||
// NOT here — they are managed only in config.toml on the server, so the UI
|
// NOT here — they are managed only in config.toml on the server, so the UI
|
||||||
// never reads or writes them. credentialStatus surfaces whether each is
|
// never reads or writes them. The Test buttons verify them.
|
||||||
// configured, and the Test buttons verify them.
|
|
||||||
var settingsKeys = []string{
|
var settingsKeys = []string{
|
||||||
"ebay_daily_call_limit",
|
"ebay_daily_call_limit",
|
||||||
"ntfy_base_url",
|
"ntfy_base_url",
|
||||||
@@ -29,29 +28,6 @@ var settingsKeys = []string{
|
|||||||
"match_confidence_threshold",
|
"match_confidence_threshold",
|
||||||
"apify_cost_per_call",
|
"apify_cost_per_call",
|
||||||
"monthly_budget_usd",
|
"monthly_budget_usd",
|
||||||
"resend_from",
|
|
||||||
}
|
|
||||||
|
|
||||||
// credentialStatus reports, per credential, whether it is configured in
|
|
||||||
// config.toml — without exposing the secret itself. ntfy_auth reflects the
|
|
||||||
// Basic-auth user/password pair.
|
|
||||||
func (a *App) credentialStatus() map[string]string {
|
|
||||||
configured := map[string]bool{
|
|
||||||
"apify_api_key": strings.TrimSpace(a.Cfg.Apify.APIKey) != "",
|
|
||||||
"ebay_client_id": strings.TrimSpace(a.Cfg.Ebay.ClientID) != "",
|
|
||||||
"ebay_client_secret": strings.TrimSpace(a.Cfg.Ebay.ClientSecret) != "",
|
|
||||||
"ntfy_auth": strings.TrimSpace(a.Cfg.Ntfy.Username) != "" && a.Cfg.Ntfy.Password != "",
|
|
||||||
"resend_api_key": strings.TrimSpace(a.Cfg.Resend.APIKey) != "",
|
|
||||||
}
|
|
||||||
status := make(map[string]string, len(configured))
|
|
||||||
for k, ok := range configured {
|
|
||||||
if ok {
|
|
||||||
status[k] = "Set in config.toml"
|
|
||||||
} else {
|
|
||||||
status[k] = "Not set"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) settingsData(r *http.Request) (templates.SettingsData, error) {
|
func (a *App) settingsData(r *http.Request) (templates.SettingsData, error) {
|
||||||
@@ -69,7 +45,6 @@ func (a *App) settingsData(r *http.Request) (templates.SettingsData, error) {
|
|||||||
return templates.SettingsData{
|
return templates.SettingsData{
|
||||||
Page: a.page(r, "Settings", "settings"),
|
Page: a.page(r, "Settings", "settings"),
|
||||||
Values: values,
|
Values: values,
|
||||||
CredentialStatus: a.credentialStatus(),
|
|
||||||
IsAdmin: cur != nil && cur.Role == models.RoleAdmin,
|
IsAdmin: cur != nil && cur.Role == models.RoleAdmin,
|
||||||
Users: users,
|
Users: users,
|
||||||
EbayUsedToday: ebayUsed,
|
EbayUsedToday: ebayUsed,
|
||||||
@@ -150,10 +125,7 @@ func (a *App) PostTestResend(w http.ResponseWriter, r *http.Request) {
|
|||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
apiKey = a.Cfg.Resend.APIKey
|
apiKey = a.Cfg.Resend.APIKey
|
||||||
}
|
}
|
||||||
from := strings.TrimSpace(d.Values["resend_from"])
|
from := a.Cfg.Resend.From
|
||||||
if from == "" {
|
|
||||||
from = a.Cfg.Resend.From
|
|
||||||
}
|
|
||||||
to := ""
|
to := ""
|
||||||
if cur != nil {
|
if cur != nil {
|
||||||
to = cur.Email
|
to = cur.Email
|
||||||
|
|||||||
@@ -21,11 +21,7 @@ func (s *Scheduler) emailClient(ctx context.Context) *email.Client {
|
|||||||
if v, _ := s.store.GetSetting(ctx, "resend_api_key"); v != "" {
|
if v, _ := s.store.GetSetting(ctx, "resend_api_key"); v != "" {
|
||||||
apiKey = v
|
apiKey = v
|
||||||
}
|
}
|
||||||
from := s.cfg.Resend.From
|
return email.New(apiKey, s.cfg.Resend.From)
|
||||||
if v, _ := s.store.GetSetting(ctx, "resend_from"); v != "" {
|
|
||||||
from = v
|
|
||||||
}
|
|
||||||
return email.New(apiKey, from)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dealMailer resolves the item owner and Resend client for deal-alert email.
|
// dealMailer resolves the item owner and Resend client for deal-alert email.
|
||||||
|
|||||||
@@ -8,12 +8,8 @@ import (
|
|||||||
|
|
||||||
type SettingsData struct {
|
type SettingsData struct {
|
||||||
Page
|
Page
|
||||||
Values map[string]string
|
Values map[string]string
|
||||||
// CredentialStatus maps each secret settings key to a human-readable
|
IsAdmin bool
|
||||||
// status ("Saved in settings", "Set in config.toml", "Not set"). Secret
|
|
||||||
// values are never rendered into the form itself.
|
|
||||||
CredentialStatus map[string]string
|
|
||||||
IsAdmin bool
|
|
||||||
Users []models.User
|
Users []models.User
|
||||||
TestNtfyOK string
|
TestNtfyOK string
|
||||||
TestApifyOK string
|
TestApifyOK string
|
||||||
@@ -48,16 +44,6 @@ func (d SettingsData) EbayLimitReached() bool {
|
|||||||
return d.EbayDailyLimit > 0 && d.EbayUsedToday >= d.EbayDailyLimit
|
return d.EbayDailyLimit > 0 && d.EbayUsedToday >= d.EbayDailyLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
// credStatus renders the "Saved in settings / Set in config.toml / Not set"
|
|
||||||
// indicator for a secret field without ever printing the secret itself.
|
|
||||||
templ credStatus(d SettingsData, key string) {
|
|
||||||
if s := d.CredentialStatus[key]; s != "" {
|
|
||||||
<div class="v-muted text-xs mt-1">
|
|
||||||
Status: { s }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templ settingsBody(d SettingsData) {
|
templ settingsBody(d SettingsData) {
|
||||||
<div class="space-y-8 max-w-3xl">
|
<div class="space-y-8 max-w-3xl">
|
||||||
<div>
|
<div>
|
||||||
@@ -72,18 +58,6 @@ templ settingsBody(d SettingsData) {
|
|||||||
</div>
|
</div>
|
||||||
<form method="post" action="/settings" class="space-y-4">
|
<form method="post" action="/settings" class="space-y-4">
|
||||||
@CSRFInput(d.CSRFToken)
|
@CSRFInput(d.CSRFToken)
|
||||||
<div>
|
|
||||||
<label class="v-label">Apify API Key</label>
|
|
||||||
@credStatus(d, "apify_api_key")
|
|
||||||
</div>
|
|
||||||
<div class="border-t border-white/10 pt-4">
|
|
||||||
<label class="v-label">eBay App ID (Client ID)</label>
|
|
||||||
@credStatus(d, "ebay_client_id")
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="v-label">eBay Cert ID (Client Secret)</label>
|
|
||||||
@credStatus(d, "ebay_client_secret")
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="v-label">eBay Daily Call Limit</label>
|
<label class="v-label">eBay Daily Call Limit</label>
|
||||||
<input class="v-input font-mono" name="ebay_daily_call_limit" type="number" value={ d.Values["ebay_daily_call_limit"] } placeholder="5000 (blank uses config default)"/>
|
<input class="v-input font-mono" name="ebay_daily_call_limit" type="number" value={ d.Values["ebay_daily_call_limit"] } placeholder="5000 (blank uses config default)"/>
|
||||||
@@ -107,10 +81,6 @@ templ settingsBody(d SettingsData) {
|
|||||||
<label class="v-label">Ntfy Default Topic</label>
|
<label class="v-label">Ntfy Default Topic</label>
|
||||||
<input class="v-input" name="ntfy_default_topic" value={ d.Values["ntfy_default_topic"] }/>
|
<input class="v-input" name="ntfy_default_topic" value={ d.Values["ntfy_default_topic"] }/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="v-label">Ntfy Auth (user / password)</label>
|
|
||||||
@credStatus(d, "ntfy_auth")
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="v-label">Global Poll Interval (minutes)</label>
|
<label class="v-label">Global Poll Interval (minutes)</label>
|
||||||
<input class="v-input font-mono" name="global_poll_interval_minutes" type="number" value={ d.Values["global_poll_interval_minutes"] }/>
|
<input class="v-input font-mono" name="global_poll_interval_minutes" type="number" value={ d.Values["global_poll_interval_minutes"] }/>
|
||||||
@@ -119,15 +89,6 @@ templ settingsBody(d SettingsData) {
|
|||||||
<label class="v-label">Match Confidence Threshold</label>
|
<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"] }/>
|
<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>
|
||||||
<div class="border-t border-white/10 pt-4">
|
|
||||||
<label class="v-label">Resend API Key</label>
|
|
||||||
@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">
|
<div class="border-t border-white/10 pt-4">
|
||||||
<label class="v-label">Estimated Apify Cost per Call (USD)</label>
|
<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"] }/>
|
<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"] }/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user