refactor(server): use shared models, provision and indexes

Also removes the duplicate Slugify in stepscan.go; workflow step slugs now
use the shared definition too.
This commit is contained in:
2026-07-24 13:42:44 +01:00
parent b335dc77e3
commit 76d3111a72
10 changed files with 75 additions and 197 deletions
+2 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/google/uuid"
"github.com/mrhid6/vantage/server/internal/db"
"github.com/mrhid6/vantage/server/internal/models"
"github.com/mrhid6/vantage/shared/provision"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
@@ -41,7 +42,7 @@ func readDefaultStepFiles() ([]models.WorkflowStep, error) {
continue
}
s.Source = "default"
s.Slug = Slugify(s.Name)
s.Slug = provision.Slugify(s.Name)
if s.Slug == "" {
continue
}
+7 -11
View File
@@ -9,6 +9,7 @@ import (
"github.com/google/uuid"
"github.com/mrhid6/vantage/server/internal/db"
"github.com/mrhid6/vantage/server/internal/models"
"github.com/mrhid6/vantage/shared/indexes"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
@@ -22,21 +23,16 @@ var scopedCollections = []string{
}
func EnsureAuthIndexes() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if _, err := db.Col("users").Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{Key: "email", Value: 1}},
Options: options.Index().SetUnique(true),
}); err != nil {
return err
}
if _, err := db.Col("orgs").Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{Key: "slug", Value: 1}},
Options: options.Index().SetUnique(true),
}); err != nil {
// users.email and orgs.slug are declared in the shared module so the
// control plane and sitesvc cannot disagree about them.
if err := indexes.EnsureCoreIndexes(ctx, db.Database); err != nil {
return err
}
// org_oidc is control-plane only, so its index stays here.
if _, err := db.Col("org_oidc").Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{Key: "org_id", Value: 1}},
Options: options.Index().SetUnique(true),
+12 -42
View File
@@ -6,18 +6,13 @@ import (
"log"
"time"
"github.com/google/uuid"
"github.com/mrhid6/vantage/server/internal/db"
"github.com/mrhid6/vantage/server/internal/models"
"github.com/mrhid6/vantage/shared/provision"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
var reservedSlugs = map[string]bool{
"www": true, "api": true, "app": true, "admin": true, "auth": true,
"install": true, "static": true, "_next": true, "default": true,
}
func GetOrg(orgID string) (*models.Org, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -86,11 +81,11 @@ func AdoptOrg(orgID, name string) (*models.Org, error) {
set := bson.M{"name": name}
slug := Slugify(name)
if len(slug) > 40 {
slug = slug[:40]
slug := provision.Slugify(name)
if len(slug) > provision.MaxSlugLength {
slug = slug[:provision.MaxSlugLength]
}
if len(slug) >= 3 && !reservedSlugs[slug] {
if len(slug) >= provision.MinSlugLength && !provision.ReservedSlugs[slug] {
n, err := db.Col("orgs").CountDocuments(ctx, bson.M{"slug": slug, "org_id": bson.M{"$ne": orgID}})
if err != nil {
return nil, err
@@ -109,45 +104,20 @@ func AdoptOrg(orgID, name string) (*models.Org, error) {
return GetOrg(orgID)
}
// CreateOrg creates an organisation and seeds its default workflow steps.
//
// The creation rules live in shared/provision because sitesvc creates
// organisations too. Seeding stays here: shared must not know about workflow
// steps.
func CreateOrg(name string) (*models.Org, error) {
base := Slugify(name)
if len(base) < 3 {
return nil, fmt.Errorf("organization name too short (slug must be >= 3 chars)")
}
if len(base) > 40 {
base = base[:40]
}
if reservedSlugs[base] {
return nil, fmt.Errorf("organization name is reserved")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
slug := base
for i := 2; ; i++ {
n, err := db.Col("orgs").CountDocuments(ctx, bson.M{"slug": slug})
if err != nil {
return nil, err
}
if n == 0 {
break
}
slug = fmt.Sprintf("%s-%d", base, i)
}
o := &models.Org{OrgID: uuid.NewString(), Name: name, Slug: slug, CreatedAt: time.Now()}
if _, err := db.Col("orgs").InsertOne(ctx, o); err != nil {
if mongo.IsDuplicateKeyError(err) {
return nil, fmt.Errorf("organization slug already taken")
}
o, err := provision.CreateOrg(ctx, db.Database, name)
if err != nil {
return nil, err
}
if created, updated, err := SeedDefaultSteps(o.OrgID); err != nil {
log.Printf("warning: failed to seed default steps for new org %s: %v", o.OrgID, err)
} else {
+2 -7
View File
@@ -34,11 +34,6 @@ func DeriveOutputs(script string) []string {
return out
}
var slugStrip = regexp.MustCompile(`[^a-z0-9]+`)
// Slugify lived here and was mirrored by hand in sitesvc. It now has a single
// definition in shared/provision, which both services import.
func Slugify(name string) string {
s := strings.ToLower(name)
s = slugStrip.ReplaceAllString(s, "-")
return strings.Trim(s, "-")
}
+8 -30
View File
@@ -7,11 +7,10 @@ import (
"strings"
"time"
"github.com/google/uuid"
"github.com/mrhid6/vantage/server/internal/db"
"github.com/mrhid6/vantage/server/internal/models"
"github.com/mrhid6/vantage/shared/provision"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"golang.org/x/crypto/bcrypt"
)
@@ -58,37 +57,16 @@ func GetUserInOrg(orgID, userID string) (*models.User, error) {
}
func CreateUser(orgID, email, password, role, authSource string) (*models.User, error) {
email = strings.ToLower(strings.TrimSpace(email))
if email == "" {
return nil, fmt.Errorf("email required")
}
if !models.ValidRole(role) {
return nil, fmt.Errorf("invalid role %q", role)
}
u := &models.User{
UserID: uuid.NewString(),
OrgID: orgID,
Email: email,
Role: role,
AuthSource: authSource,
CreatedAt: time.Now(),
}
if password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return nil, err
}
u.PasswordHash = string(hash)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if _, err := db.Col("users").InsertOne(ctx, u); err != nil {
if mongo.IsDuplicateKeyError(err) {
return nil, fmt.Errorf("email already registered")
}
return nil, err
u, err := provision.CreateUser(ctx, db.Database, orgID, email, password, role, authSource)
if errors.Is(err, provision.ErrEmailTaken) {
// Preserve the exact error string the API returned before this call
// was delegated to the shared module.
return nil, fmt.Errorf("email already registered")
}
return u, nil
return u, err
}
func GetUserByEmail(email string) (*models.User, error) {