feat(admin): self-hosted customer accounts with email verification
Mirrors the pattern sitesvc already proves: 32 random bytes, only the SHA-256 hash stored, a 24-hour expiry, and the token cleared on use -- so a leaked database yields no working links. Unverified login returns a distinct "verify your email address first" rather than the generic error. The address is already known to be theirs, so there is nothing to disclose and that is the only useful thing to say. Licence blobs are emailed inline. A blob is signed public data, not a secret: it is useless on any instance other than the one it names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// Package mail delivers verification links and licence files.
|
||||
package mail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host, Port, From, Username, Password string
|
||||
PublicURL string
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
func Init(c Config) { cfg = c }
|
||||
|
||||
func Enabled() bool { return cfg.Host != "" && cfg.From != "" }
|
||||
|
||||
func send(to, subject, body string) error {
|
||||
if !Enabled() {
|
||||
return fmt.Errorf("SMTP is not configured")
|
||||
}
|
||||
msg := strings.Join([]string{
|
||||
"From: " + cfg.From,
|
||||
"To: " + to,
|
||||
"Subject: " + subject,
|
||||
"MIME-Version: 1.0",
|
||||
"Content-Type: text/plain; charset=utf-8",
|
||||
"", body,
|
||||
}, "\r\n")
|
||||
|
||||
var auth smtp.Auth
|
||||
if cfg.Username != "" {
|
||||
auth = smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
|
||||
}
|
||||
return smtp.SendMail(cfg.Host+":"+cfg.Port, auth, cfg.From, []string{to}, []byte(msg))
|
||||
}
|
||||
|
||||
func SendVerification(to, token string) error {
|
||||
link := fmt.Sprintf("%s/verify?token=%s", cfg.PublicURL, token)
|
||||
return send(to, "Verify your Vantage account",
|
||||
"Confirm your email address to finish setting up your Vantage account:\n\n"+
|
||||
link+"\n\nThis link expires in 24 hours.\n")
|
||||
}
|
||||
|
||||
// SendLicense delivers the blob inline. It is signed public data, not a secret —
|
||||
// it is useless on any instance other than the one it names.
|
||||
func SendLicense(to, instanceName, blob string) error {
|
||||
return send(to, "Your Vantage licence key",
|
||||
fmt.Sprintf("Your licence for %s is below.\n\n"+
|
||||
"Paste it into Settings → Licence on your Vantage install:\n\n%s\n",
|
||||
instanceName, blob))
|
||||
}
|
||||
Reference in New Issue
Block a user