Files
selfpost/internal/web/view/account.go
T
mixandClaude Opus 5.5 3f5cd9adf0
test / test (push) Canceled after 0s
notification-mail-recheck: Re-check DNS on Server › Settings
The owner, 2026-10-16: "Да, бери notification-mail-recheck". The records
of the server's own name can be checked again where they are shown: Re-
check DNS in Settings' page head (mockup 98e4dfc) posts to
POST /server/settings/dns-recheck (guard 52a9811), global role, which
runs Health's lookups - the server's address, then serverMail(true) - and
returns to /server/settings?rechecked=1, "DNS re-checked.". The
Notification mail box's end slot says when the records were checked
("The server's own name · checked 3 min ago", from the check's
CheckedAt; the old wording while nothing was looked up). A record that
has turned right comes back folded.

Tests: the page head's form and the end slot, and the Settings form
holding neither the re-check nor the right column
(TestSettingsRechecksTheMailRecords); the re-check skips the cache,
redirects and the page shows both records folded
(TestSettingsRecheckAsksForNotificationMailAgain); a domain
administrator gets 404 (globalOnlyRoutes). go vet + go test (26
packages) and make e2e on the test server for this tree.

Docs: architecture.md routes, the panel-redesign plan's route table,
guide § Settings, help (help.go word for word with the mockup's);
CHANGELOG [Unreleased]; roadmap row done. The roadmap's settings-save-row
note no longer claims the left-column form was needed for this - the
action went to the page head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:29:54 +03:00

168 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package view
// The data of Account (templates/account.html, mockup account.html) and of
// Server › Settings (templates/settings.html, mockup settings.html): what used
// to be one Settings page, split by whose it is. Account is the signed-in
// user's own — who they are to the panel and their password — and every role
// has it. Settings is the instance's.
import "strconv"
// Account is the data of the Account page. Two forms, each its own POST: the
// profile (Username, Email) and the password. The fields hold what was stored,
// or what was just submitted when a form is shown again with an error.
type Account struct {
Meta
Head Head
Flash *Flash
Profile Box
Username string
Email string
// Notify is whether the panel writes to this user: the checkbox under the
// e-mail.
Notify bool
Password Box
}
// NewAccount builds the page for a user. role is "global" or "domain" and
// appears in the kicker.
func NewAccount(m Meta, role string) *Account {
m.Title, m.Section, m.Page = "Account", "user", "account"
return &Account{
Meta: m,
Head: Head{
Kicker: "Signed in as " + m.User + " · " + role,
Title: "Account",
Lead: Plain("Who you are to this panel. Applications keep their own logins and passwords and are not affected."),
},
Profile: Box{No: "01", Title: "Profile"},
Password: Box{No: "02", Title: "Password"},
}
}
// WithResult shows the result of the last action, or its refusal, between the
// head and the first box.
func (a *Account) WithResult(flash, formErr string) *Account {
a.Flash = resultFlash(flash, formErr)
return a
}
// Settings is the data of Server › Settings: one form with the instance's
// settings and one Save settings button, and the read-only level-1 rate limit
// the operator sets in .env.
type Settings struct {
Meta
Head Head
Flash *Flash
Retention Box
RetentionDays string
// Inbound is whether the inbound relay is on, which gives the retention box
// its second field; InboundRetentionDays is what that field shows.
Inbound bool
InboundRetentionDays string
RateLimits Box
RateFacts []Fact
// Notifications is the instance switch for notification e-mails; Notify is
// its state and NotifyNote the line under it, which says from where and to
// whom.
Notifications Box
Notify bool
NotifyNote Text
// Mail holds the DNS records of the server's own name, which notifications
// are signed and authorised with, drawn like a domain's.
Mail Box
MailRecords []Record
}
// NewSettings builds the page. retention is what the field shows (the stored
// value, or what was just submitted); level1 is the per-client-IP limit as
// FormatRate writes it.
func NewSettings(m Meta, retention, level1 string) *Settings {
m.Title, m.Section, m.Page = "Settings", "server", "settings"
return &Settings{
Meta: m,
Head: Head{
Kicker: "Server",
Title: "Settings",
Lead: Rich("What holds for the whole instance, whoever is signed in. Your own name, e-mail and password are under ",
Link("/account", "Account"), "."),
Actions: []Action{{Label: "Re-check DNS", Icon: "ti-refresh", Post: settingsDNSRecheck}},
},
Retention: Box{No: "01", Title: "Log retention", Help: topicLink(HelpSettings, "How long rows are kept")},
RetentionDays: retention,
RateLimits: Box{No: "04", Title: "Sending rate limits", ID: "rate-limits", End: Plain("Read-only"), Help: topicLink(HelpLimits, "Levels 1 and 2")},
RateFacts: []Fact{
{Label: "Level 1 · per client IP", Value: Plain(level1), Big: true,
Note: Rich("Set in ", Code(".env"), "; restart to change. Nothing below may exceed it.")},
{Label: "Level 2 · domain, application",
Value: Rich("Set on each ", Link("/outbound/domains", "domain"), " and on each application of a domain.")},
},
Notifications: Box{No: "02", Title: "Notifications", ID: "notifications", Help: topicLink(HelpNotifications, "What is sent and when")},
Mail: Box{No: "03", Title: "Notification mail", ID: "notification-mail", End: Plain("DNS records of the server's own name"),
Help: topicLink(HelpNotifications, "What the panel sends and what makes it arrive")},
}
}
// WithNotifications sets the instance switch: on is whether notification
// e-mails are sent at all, from is the address they come from.
func (s *Settings) WithNotifications(on bool, from string) *Settings {
s.Notify = on
s.NotifyNote = Rich("From ", Mono(from), ", to every user who has an e-mail and has not turned them off under ",
Link("/account", "Account"), ".")
return s
}
// settingsDNSRecheck is where Re-check DNS on Settings posts.
const settingsDNSRecheck = "/server/settings/dns-recheck"
// WithMail sets the records of the server's own name, in the order they are
// shown (ServerMailDKIM, ServerMailSPF), and how long ago they were checked
// (FormatAge; "" when they were not).
func (s *Settings) WithMail(records []Record, checked string) *Settings {
s.MailRecords = records
s.Mail.End = Plain("DNS records of the server's own name")
if checked != "" {
s.Mail.End = Plain("The server's own name · checked " + checked)
}
return s
}
// WithInbound adds the retention of the Inbound log to the Log retention box.
// days is what the field shows, like retention in NewSettings. The field exists
// only while the inbound relay is on: with it off there is no journal to keep.
func (s *Settings) WithInbound(days string) *Settings {
s.Inbound, s.InboundRetentionDays = true, days
return s
}
// WithResult shows the result of the last action, or its refusal, between the
// head and the form.
func (s *Settings) WithResult(flash, formErr string) *Settings {
s.Flash = resultFlash(flash, formErr)
return s
}
// FormatRate writes a rate limit of messages per window the short way:
// "600 / h" for an hourly limit, "30 / min", "5 000 / day", and the window in
// seconds for any other.
func FormatRate(messages, windowSeconds int) string {
n := FormatCount(int64(messages))
switch {
case windowSeconds == 60:
return n + " / min"
case windowSeconds == 3600:
return n + " / h"
case windowSeconds == 86400:
return n + " / day"
case windowSeconds > 0 && windowSeconds%3600 == 0:
return n + " / " + strconv.Itoa(windowSeconds/3600) + " h"
}
return n + " / " + strconv.Itoa(windowSeconds) + " s"
}