refactor(sitesvc): rename Org to Instance, refuse an unmigrated database

The signup form's JSON field becomes instance_name, and the pending-signup
document field with it. That collection is sitesvc-private and expires after
24 hours, so no migration is needed, but in-flight signups written before the
deploy will fail verification.
This commit is contained in:
2026-07-24 14:00:11 +01:00
parent 3891a2c239
commit 3f0f12b111
7 changed files with 70 additions and 63 deletions
-6
View File
@@ -5,10 +5,6 @@ import (
"time"
)
type limiter struct {
mu sync.Mutex
hits map[string]*window
@@ -51,8 +47,6 @@ func (l *limiter) allow(key string) bool {
return true
}
func (l *limiter) gc(now time.Time) {
if now.Sub(l.lastGC) < l.window {
return
+15 -15
View File
@@ -22,10 +22,10 @@ const (
)
type signupBody struct {
OrgName string `json:"org_name"`
Email string `json:"email"`
Password string `json:"password"`
Website string `json:"website"`
InstanceName string `json:"instance_name"`
Email string `json:"email"`
Password string `json:"password"`
Website string `json:"website"`
}
func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
@@ -41,7 +41,7 @@ func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
var problems []fieldError
orgName, nameErr := text("org_name", body.OrgName, true, maxShort)
instanceName, nameErr := text("instance_name", body.InstanceName, true, maxShort)
if nameErr != nil {
problems = append(problems, *nameErr)
}
@@ -82,7 +82,7 @@ func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
token, err := store.CreatePending(ctx, orgName, addr, body.Password)
token, err := store.CreatePending(ctx, instanceName, addr, body.Password)
switch {
case errors.Is(err, store.ErrEmailTaken):
writeJSON(w, http.StatusConflict, map[string]any{
@@ -95,7 +95,7 @@ func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
return
case errors.Is(err, store.ErrNameRejected):
writeFieldErrors(w, []fieldError{{
Field: "org_name",
Field: "instance_name",
Message: strings.TrimPrefix(err.Error(), store.ErrNameRejected.Error()+": "),
}})
return
@@ -108,7 +108,7 @@ func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
}
link := s.verifyURL(token)
if err := s.mail.SendVerification(addr, orgName, link, store.PendingTTL); err != nil {
if err := s.mail.SendVerification(addr, instanceName, link, store.PendingTTL); err != nil {
log.Printf("signup: send verification to %s: %v", addr, err)
writeJSON(w, http.StatusBadGateway, map[string]string{
@@ -136,7 +136,7 @@ func (s *Server) handleVerify(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
org, err := store.Verify(ctx, token)
inst, err := store.Verify(ctx, token)
switch {
case errors.Is(err, store.ErrBadToken):
s.verifyPage(w, http.StatusGone, "Link expired",
@@ -157,23 +157,23 @@ func (s *Server) handleVerify(w http.ResponseWriter, r *http.Request) {
return
}
log.Printf("verify: provisioned org %s (%s)", org.Slug, org.OrgID)
log.Printf("verify: provisioned instance %s (%s)", inst.Slug, inst.InstanceID)
if login := s.loginURL(org.Slug); login != "" {
if login := s.loginURL(inst.Slug); login != "" {
http.Redirect(w, r, login, http.StatusSeeOther)
return
}
s.verifyPage(w, http.StatusOK, "Organisation ready",
fmt.Sprintf("%s is set up and you are its owner. You can sign in now.", org.Name))
fmt.Sprintf("%s is set up and you are its owner. You can sign in now.", inst.Name))
}
// loginURL is the org-specific sign-in URL a verified owner is sent to. Each
// org lives on its own subdomain (<slug>.vantage.hostxtra.co.uk), so the slug
// loginURL is the instance-specific sign-in URL a verified owner is sent to. Each
// instance lives on its own subdomain (<slug>.vantage.hostxtra.co.uk), so the slug
// must be substituted per signup rather than pointing at one shared address.
//
// APP_LOGIN_URL is a template. A "{slug}" placeholder is replaced with the
// org's slug; a value without one is treated as a literal (a single shared
// instance's slug; a value without one is treated as a literal (a single shared
// login page) so a plain URL still works. An empty value falls back to the
// confirmation page.
func (s *Server) loginURL(slug string) string {
-8
View File
@@ -7,8 +7,6 @@ import (
"unicode/utf8"
)
var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s.]+\.[^@\s]+$`)
const (
@@ -23,9 +21,6 @@ type fieldError struct {
func (e fieldError) Error() string { return e.Field + ": " + e.Message }
func text(name, value string, required bool, max int) (string, *fieldError) {
v := strings.TrimSpace(value)
if v == "" {
@@ -55,9 +50,6 @@ func email(name, value string) (string, *fieldError) {
return v, nil
}
func oneOf(name, value string, allowed []string) (string, *fieldError) {
v := strings.TrimSpace(value)
for _, a := range allowed {