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:
mrhid6
2026-07-25 19:07:47 +01:00
co-authored by Claude Opus 5
parent 769839a70d
commit 07a3756b18
3 changed files with 197 additions and 3 deletions
+10
View File
@@ -16,6 +16,7 @@ import (
"github.com/mrhid6/vantage/admin/internal/db"
"github.com/mrhid6/vantage/admin/internal/inject"
"github.com/mrhid6/vantage/admin/internal/licensing"
"github.com/mrhid6/vantage/admin/internal/mail"
"github.com/mrhid6/vantage/admin/internal/models"
)
@@ -29,6 +30,15 @@ func main() {
licensing.SetSigningKey(cfg.SigningKey)
mail.Init(mail.Config{
Host: cfg.SMTPHost, Port: cfg.SMTPPort, From: cfg.SMTPFrom,
Username: cfg.SMTPUsername, Password: cfg.SMTPPassword,
PublicURL: cfg.PublicURL,
})
if !mail.Enabled() {
log.Println("warning: SMTP not configured; verification and licence emails will fail")
}
auth.InitRedis(cfg.RedisAddr)
pingCtx, pingCancel := context.WithTimeout(context.Background(), 10*time.Second)
if err := auth.Ping(pingCtx); err != nil {