This commit is contained in:
@@ -15,14 +15,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxBodyBytes = 32 << 10
|
||||
maxBodyBytes = 32 << 10
|
||||
perIPLimit = 5
|
||||
perIPWindow = 10 * time.Minute
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
type Server struct {
|
||||
mail mail.Config
|
||||
limiter *limiter
|
||||
@@ -49,7 +46,7 @@ func (s *Server) Routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /api/contact", s.handleContact)
|
||||
mux.HandleFunc("POST /api/signup", s.handleSignup)
|
||||
|
||||
|
||||
mux.HandleFunc("GET /api/verify", s.handleVerify)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
@@ -57,8 +54,6 @@ func (s *Server) Routes() http.Handler {
|
||||
return s.withCORS(mux)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func parseOrigins(raw string) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, o := range strings.Split(raw, ",") {
|
||||
@@ -69,10 +64,6 @@ func parseOrigins(raw string) map[string]bool {
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func (s *Server) withCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
@@ -91,9 +82,6 @@ func (s *Server) withCORS(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func (s *Server) clientIP(r *http.Request) string {
|
||||
if s.trustProxy {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
@@ -110,15 +98,13 @@ func (s *Server) clientIP(r *http.Request) string {
|
||||
return host
|
||||
}
|
||||
|
||||
|
||||
|
||||
type contactBody struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Servers string `json:"servers"`
|
||||
Topic string `json:"topic"`
|
||||
Message string `json:"message"`
|
||||
Website string `json:"website"`
|
||||
Website string `json:"website"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -137,8 +123,6 @@ func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
if strings.TrimSpace(body.Website) != "" {
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"status": "received"})
|
||||
return
|
||||
@@ -201,9 +185,6 @@ func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if err := s.mail.Send(subject(addr, fields), plainBody(addr, fields), addr); err != nil {
|
||||
log.Printf("contact send: %v", err)
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{
|
||||
@@ -216,7 +197,7 @@ func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func subject(addr string, fields map[string]string) string {
|
||||
return fmt.Sprintf("[Vantage] %s — %s", fields["topic"], addr)
|
||||
return fmt.Sprintf("[Vantage] %s %s", fields["topic"], addr)
|
||||
}
|
||||
|
||||
func plainBody(addr string, fields map[string]string) string {
|
||||
@@ -233,8 +214,6 @@ func plainBody(addr string, fields map[string]string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
|
||||
|
||||
func decode(w http.ResponseWriter, r *http.Request, dst any) bool {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
||||
dec := json.NewDecoder(r.Body)
|
||||
|
||||
@@ -25,20 +25,15 @@ type signupBody struct {
|
||||
OrgName string `json:"org_name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Website string `json:"website"`
|
||||
Website string `json:"website"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
|
||||
var body signupBody
|
||||
if !decode(w, r, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
if strings.TrimSpace(body.Website) != "" {
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"status": "check_email"})
|
||||
return
|
||||
@@ -114,9 +109,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 {
|
||||
|
||||
|
||||
|
||||
|
||||
log.Printf("signup: send verification to %s: %v", addr, err)
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{
|
||||
"error": "We could not send the confirmation email. Check the address, or email support@hostxtra.co.uk.",
|
||||
@@ -132,9 +125,6 @@ func (s *Server) verifyURL(token string) string {
|
||||
return fmt.Sprintf("%s/api/verify?token=%s", base, url.QueryEscape(token))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func (s *Server) handleVerify(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
@@ -178,9 +168,6 @@ func (s *Server) handleVerify(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Sprintf("%s is set up and you are its owner. You can sign in now.", org.Name))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func (s *Server) verifyPage(w http.ResponseWriter, status int, heading, detail string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
@@ -193,7 +180,7 @@ func (s *Server) verifyPage(w http.ResponseWriter, status int, heading, detail s
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>%s — Vantage</title>
|
||||
<title>%s Vantage</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body {
|
||||
|
||||
@@ -15,8 +15,6 @@ import (
|
||||
|
||||
const timeout = 15 * time.Second
|
||||
|
||||
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
@@ -41,19 +39,10 @@ func (c Config) Enabled() bool {
|
||||
return c.Host != "" && c.From != "" && c.To != ""
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func (c Config) Send(subject, body, replyTo string) error {
|
||||
return c.sendTo(c.To, subject, body, replyTo)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (c Config) sendTo(to, subject, body, replyTo string) error {
|
||||
if !c.Enabled() {
|
||||
return fmt.Errorf("smtp: not configured")
|
||||
@@ -127,9 +116,6 @@ func recipients(to string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func message(from, to, subject, body, replyTo string) []byte {
|
||||
var b strings.Builder
|
||||
b.WriteString("From: " + sanitizeHeader(from) + "\r\n")
|
||||
@@ -137,10 +123,7 @@ func message(from, to, subject, body, replyTo string) []byte {
|
||||
if replyTo != "" {
|
||||
b.WriteString("Reply-To: " + sanitizeHeader(replyTo) + "\r\n")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
b.WriteString("Date: " + time.Now().Format(time.RFC1123Z) + "\r\n")
|
||||
b.WriteString("Message-ID: " + messageID(from) + "\r\n")
|
||||
b.WriteString("Subject: " + mime.QEncoding.Encode("utf-8", sanitizeHeader(subject)) + "\r\n")
|
||||
@@ -151,9 +134,6 @@ func message(from, to, subject, body, replyTo string) []byte {
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func messageID(from string) string {
|
||||
domain := "vantage.local"
|
||||
if at := strings.LastIndex(from, "@"); at >= 0 && at < len(from)-1 {
|
||||
@@ -170,9 +150,6 @@ func sanitizeHeader(v string) string {
|
||||
return strings.NewReplacer("\r", " ", "\n", " ").Replace(v)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func (c Config) SendVerification(to, orgName, link string, ttl time.Duration) error {
|
||||
body := fmt.Sprintf(`Confirm your email to finish creating %s on Vantage.
|
||||
|
||||
@@ -181,7 +158,7 @@ Open this link:
|
||||
%s
|
||||
|
||||
The link works once and expires in %d hours. Until you use it, no account
|
||||
exists — nothing has been created and the address is not registered.
|
||||
exists nothing has been created and the address is not registered.
|
||||
|
||||
If you did not request this, ignore this email and nothing will happen.
|
||||
`, orgName, link, int(ttl.Hours()))
|
||||
|
||||
@@ -15,7 +15,7 @@ with no dependency on the server. That is a deliberate trade: sitesvc stays
|
||||
small and independent, at the cost of this one duplicated rule set.
|
||||
|
||||
Keep the two in step. If the control plane's slug handling, reserved names or
|
||||
bcrypt cost change, change them here in the same commit — nothing enforces the
|
||||
bcrypt cost change, change them here in the same commit nothing enforces the
|
||||
match automatically, and a divergence would create tenants under rules the app
|
||||
does not agree with.
|
||||
*/
|
||||
@@ -23,13 +23,11 @@ does not agree with.
|
||||
const (
|
||||
MinSlugLength = 3
|
||||
MaxSlugLength = 40
|
||||
BcryptCost = 12
|
||||
BcryptCost = 12
|
||||
)
|
||||
|
||||
var slugStrip = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
|
||||
|
||||
var ReservedSlugs = map[string]bool{
|
||||
"www": true, "api": true, "app": true, "admin": true, "auth": true,
|
||||
"install": true, "static": true, "_next": true, "default": true,
|
||||
@@ -41,8 +39,6 @@ func Slugify(name string) string {
|
||||
return strings.Trim(s, "-")
|
||||
}
|
||||
|
||||
|
||||
|
||||
func BaseSlug(name string) (string, error) {
|
||||
base := Slugify(name)
|
||||
if len(base) < MinSlugLength {
|
||||
@@ -57,7 +53,6 @@ func BaseSlug(name string) (string, error) {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
|
||||
func NextSlug(base string, attempt int) string {
|
||||
if attempt < 2 {
|
||||
return base
|
||||
|
||||
Reference in New Issue
Block a user