+4
-2
@@ -10,6 +10,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/mrhid6/vantage/sitesvc/internal/api"
|
||||
"github.com/mrhid6/vantage/sitesvc/internal/mail"
|
||||
"github.com/mrhid6/vantage/sitesvc/internal/store"
|
||||
@@ -21,8 +22,9 @@ import (
|
||||
// that is how the new tenant becomes visible to the app — but shares no code
|
||||
// and no process with it.
|
||||
func main() {
|
||||
// The database name comes from the URI path, e.g.
|
||||
// mongodb://user:pass@host:27017/vantage?authSource=vantage
|
||||
|
||||
godotenv.Load()
|
||||
|
||||
mongoURI := getEnv("MONGO_URI", "mongodb://localhost:27017/vantage")
|
||||
addr := ":" + getEnv("PORT", "8082")
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ go 1.26
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
go.mongodb.org/mongo-driver/v2 v2.2.2
|
||||
golang.org/x/crypto v0.54.0
|
||||
)
|
||||
|
||||
@@ -6,6 +6,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net"
|
||||
@@ -135,6 +137,12 @@ func message(from, to, subject, body, replyTo string) []byte {
|
||||
if replyTo != "" {
|
||||
b.WriteString("Reply-To: " + sanitizeHeader(replyTo) + "\r\n")
|
||||
}
|
||||
// Date and Message-ID are RFC 5322 essentials. Without them many servers
|
||||
// accept the message at SMTP time and then silently junk or drop it, and
|
||||
// SpamAssassin scores MISSING_DATE and MISSING_MID heavily — the message
|
||||
// "sends" but never lands in the inbox.
|
||||
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")
|
||||
b.WriteString("MIME-Version: 1.0\r\n")
|
||||
b.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
|
||||
@@ -143,6 +151,21 @@ func message(from, to, subject, body, replyTo string) []byte {
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
// messageID builds a unique <id@domain>, taking the domain from the From
|
||||
// address so the identifier matches the sending domain. Falls back to the host
|
||||
// name when From has no domain part.
|
||||
func messageID(from string) string {
|
||||
domain := "vantage.local"
|
||||
if at := strings.LastIndex(from, "@"); at >= 0 && at < len(from)-1 {
|
||||
domain = strings.Trim(from[at+1:], "<> ")
|
||||
}
|
||||
var buf [16]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return fmt.Sprintf("<%d@%s>", time.Now().UnixNano(), domain)
|
||||
}
|
||||
return fmt.Sprintf("<%s@%s>", hex.EncodeToString(buf[:]), domain)
|
||||
}
|
||||
|
||||
func sanitizeHeader(v string) string {
|
||||
return strings.NewReplacer("\r", " ", "\n", " ").Replace(v)
|
||||
}
|
||||
|
||||
@@ -74,14 +74,6 @@ func DatabaseName() string {
|
||||
|
||||
func col(name string) *mongo.Collection { return database.Collection(name) }
|
||||
|
||||
// EnsureIndexes builds the constraints sitesvc depends on.
|
||||
//
|
||||
// The unique indexes on users.email and orgs.slug are the same ones the control
|
||||
// plane builds at boot, and they are a security property rather than an
|
||||
// optimisation: without them a duplicate email lets an unscoped user lookup
|
||||
// match the wrong account, and a duplicate slug makes host-based org resolution
|
||||
// pick one at random. They are (re)declared here so sitesvc does not depend on
|
||||
// the server having started first. Creating an existing index is a no-op.
|
||||
func EnsureIndexes() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
@@ -119,17 +111,11 @@ func EnsureIndexes() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmailTaken reports whether an address already has an account. It is a
|
||||
// courtesy check for a clear error message; the unique index is what actually
|
||||
// enforces uniqueness at verification time.
|
||||
func EmailTaken(ctx context.Context, email string) (bool, error) {
|
||||
n, err := col("users").CountDocuments(ctx, bson.M{"email": email})
|
||||
return n > 0, err
|
||||
}
|
||||
|
||||
// CreatePending stores an unverified signup and returns the raw token for the
|
||||
// email link. Only the token's SHA-256 hash is persisted, so a leaked database
|
||||
// does not yield working verification links.
|
||||
func CreatePending(ctx context.Context, orgName, email, password string) (string, error) {
|
||||
if _, err := provision.BaseSlug(orgName); err != nil {
|
||||
return "", fmt.Errorf("%w: %s", ErrNameRejected, err.Error())
|
||||
|
||||
Reference in New Issue
Block a user