Extract monolithic seed main.go into separate files per domain: users, tracks, playlists, chat, analytics, marketplace, social, content, live, moderation, notifications, and misc. Add config, fake data helpers, and utility modules. Update Makefile targets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
376 lines
12 KiB
Go
376 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// TestAccount represents a predefined test account.
|
|
type TestAccount struct {
|
|
Email string
|
|
Password string
|
|
Username string
|
|
DisplayName string
|
|
Role string
|
|
IsAdmin bool
|
|
Bio string
|
|
}
|
|
|
|
var testAccounts = []TestAccount{
|
|
{"admin@veza.music", "Admin123!", "admin_veza", "Admin Veza", "admin", true, "Platform administrator. Managing all the things."},
|
|
{"artist@veza.music", "Artist123!", "top_artist", "Luna Dubois", "creator", false, "Productrice electro basée à Paris. Melodic techno & ambient. Label founder."},
|
|
{"user@veza.music", "User123!", "music_fan", "Music Fan", "user", false, "Music lover. Always discovering new sounds. Playlist curator."},
|
|
{"mod@veza.music", "Mod123!", "mod_veza", "Moderator Veza", "moderator", false, "Community moderator. Keeping the vibes positive."},
|
|
{"new@veza.music", "New123!", "new_user", "New User", "user", false, "Just joined!"},
|
|
}
|
|
|
|
// SeededUser holds user data for cross-referencing in other seeders.
|
|
type SeededUser struct {
|
|
ID string
|
|
Email string
|
|
Username string
|
|
DisplayName string
|
|
Role string
|
|
IsAdmin bool
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// SeedUsers creates all users, profiles, settings, and role assignments.
|
|
// Returns the full list of seeded users for use by other seeders.
|
|
func SeedUsers(db *sql.DB, cfg Config) ([]SeededUser, error) {
|
|
fmt.Println("\n═══ USERS ═══")
|
|
|
|
// Pre-hash passwords (bcrypt cost 12)
|
|
defaultHash, err := bcrypt.GenerateFromPassword([]byte("Password123!"), 12)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bcrypt default: %w", err)
|
|
}
|
|
|
|
// Hash each test account password
|
|
testHashes := make([]string, len(testAccounts))
|
|
for i, ta := range testAccounts {
|
|
h, err := bcrypt.GenerateFromPassword([]byte(ta.Password), 12)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bcrypt test account %s: %w", ta.Email, err)
|
|
}
|
|
testHashes[i] = string(h)
|
|
}
|
|
|
|
users := make([]SeededUser, 0, cfg.TotalUsers)
|
|
|
|
// ── 1. Create test accounts first ────────────────────────────────────────
|
|
p := NewProgress("users (test accounts)", len(testAccounts))
|
|
testRows := make([][]interface{}, 0, len(testAccounts))
|
|
for i, ta := range testAccounts {
|
|
id := newUUID()
|
|
createdAt := DaysAgo(365 + (len(testAccounts)-i)*30) // Staggered creation
|
|
u := SeededUser{
|
|
ID: id,
|
|
Email: ta.Email,
|
|
Username: ta.Username,
|
|
DisplayName: ta.DisplayName,
|
|
Role: ta.Role,
|
|
IsAdmin: ta.IsAdmin,
|
|
CreatedAt: createdAt,
|
|
}
|
|
users = append(users, u)
|
|
testRows = append(testRows, []interface{}{
|
|
id, ta.Email, createdAt, // email_verified_at = created_at
|
|
testHashes[i], ta.Username, ta.Username,
|
|
ta.DisplayName, nil, nil, // first_name, last_name
|
|
ta.Bio, nil, // location
|
|
ta.Role, true, true, false, ta.IsAdmin, true,
|
|
0, nil, createdAt, 0, nil, nil,
|
|
createdAt, createdAt, nil, "{}",
|
|
})
|
|
}
|
|
_, err = BulkInsert(db, "users",
|
|
"id, email, email_verified_at, password_hash, username, slug, display_name, first_name, last_name, bio, location, role, is_active, is_verified, is_banned, is_admin, is_public, token_version, last_password_change_at, last_login_at, login_count, last_login_ip, username_changed_at, created_at, updated_at, deleted_at, social_links",
|
|
testRows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert test users: %w", err)
|
|
}
|
|
p.Update(len(testAccounts))
|
|
p.Done()
|
|
|
|
// ── 2. Generate remaining users ──────────────────────────────────────────
|
|
remaining := cfg.TotalUsers - len(testAccounts)
|
|
if remaining <= 0 {
|
|
return users, nil
|
|
}
|
|
|
|
// Distribute roles
|
|
artistCount := cfg.Artists - 1 // -1 for test artist
|
|
labelCount := cfg.Labels
|
|
modCount := cfg.Moderators - 1 // -1 for test mod
|
|
adminCount := cfg.Admins - 1 // -1 for test admin
|
|
normalCount := remaining - artistCount - labelCount - modCount - adminCount
|
|
|
|
type roleAssignment struct {
|
|
role string
|
|
isAdmin bool
|
|
count int
|
|
}
|
|
assignments := []roleAssignment{
|
|
{"admin", true, adminCount},
|
|
{"moderator", false, modCount},
|
|
{"creator", false, artistCount},
|
|
{"creator", false, labelCount}, // Labels are creators too
|
|
{"user", false, normalCount},
|
|
}
|
|
|
|
pwHash := string(defaultHash)
|
|
userIdx := len(testAccounts)
|
|
allRows := make([][]interface{}, 0, remaining)
|
|
|
|
for _, ra := range assignments {
|
|
p = NewProgress(fmt.Sprintf("users (%s)", ra.role), ra.count)
|
|
for i := 0; i < ra.count; i++ {
|
|
id := newUUID()
|
|
createdAt := RandomRegistrationTime(userIdx, cfg.TotalUsers)
|
|
createdAt = RealisticHour(createdAt)
|
|
|
|
var displayName, bio, location string
|
|
var firstName, lastName *string
|
|
|
|
if ra.role == "creator" {
|
|
displayName = GenArtistName(userIdx)
|
|
genres := GenreForArtist(userIdx)
|
|
bio = GenBio(genres[0].Name)
|
|
location = pick(cities)
|
|
fn := pick(frenchNames)
|
|
ln := pick(frenchLastNames)
|
|
firstName = &fn
|
|
lastName = &ln
|
|
} else {
|
|
fn := pick(frenchNames)
|
|
ln := pick(frenchLastNames)
|
|
displayName = fn + " " + ln
|
|
firstName = &fn
|
|
lastName = &ln
|
|
if randChance(60) {
|
|
bio = GenShortBio()
|
|
}
|
|
if randChance(40) {
|
|
location = pick(cities)
|
|
}
|
|
}
|
|
|
|
username := GenUsername(displayName, userIdx)
|
|
email := fmt.Sprintf("%s@veza-user.local", username)
|
|
|
|
// Email verification: 90% verified
|
|
var emailVerifiedAt interface{}
|
|
isVerified := randChance(90)
|
|
if isVerified {
|
|
emailVerifiedAt = RandomTimeAfter(createdAt)
|
|
}
|
|
|
|
var socialLinks string
|
|
if ra.role == "creator" && randChance(70) {
|
|
socialLinks = fmt.Sprintf(`{"website":"https://%s.com","instagram":"@%s"}`, username, username)
|
|
} else {
|
|
socialLinks = "{}"
|
|
}
|
|
|
|
var lastLoginAt interface{}
|
|
loginCount := 0
|
|
if randChance(80) {
|
|
lastLoginAt = RandomTimeAfter(createdAt)
|
|
loginCount = randInt(1, 200)
|
|
}
|
|
|
|
u := SeededUser{
|
|
ID: id,
|
|
Email: email,
|
|
Username: username,
|
|
DisplayName: displayName,
|
|
Role: ra.role,
|
|
IsAdmin: ra.isAdmin,
|
|
CreatedAt: createdAt,
|
|
}
|
|
users = append(users, u)
|
|
|
|
allRows = append(allRows, []interface{}{
|
|
id, email, emailVerifiedAt,
|
|
pwHash, username, username,
|
|
displayName, firstName, lastName,
|
|
bio, location,
|
|
ra.role, true, isVerified, false, ra.isAdmin, true,
|
|
0, nil, lastLoginAt, loginCount, nil, nil,
|
|
createdAt, createdAt, nil, socialLinks,
|
|
})
|
|
userIdx++
|
|
}
|
|
p.Update(ra.count)
|
|
p.Done()
|
|
}
|
|
|
|
p = NewProgress("users (bulk insert)", len(allRows))
|
|
_, err = BulkInsert(db, "users",
|
|
"id, email, email_verified_at, password_hash, username, slug, display_name, first_name, last_name, bio, location, role, is_active, is_verified, is_banned, is_admin, is_public, token_version, last_password_change_at, last_login_at, login_count, last_login_ip, username_changed_at, created_at, updated_at, deleted_at, social_links",
|
|
allRows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert generated users: %w", err)
|
|
}
|
|
p.Update(len(allRows))
|
|
p.Done()
|
|
|
|
// ── 3. User profiles ─────────────────────────────────────────────────────
|
|
p = NewProgress("user_profiles", len(users))
|
|
profileRows := make([][]interface{}, 0, len(users))
|
|
for _, u := range users {
|
|
var bio, tagline, website, bannerURL, avatarURL *string
|
|
language := "fr"
|
|
if randChance(30) {
|
|
language = "en"
|
|
}
|
|
theme := pick([]string{"auto", "light", "dark"})
|
|
visibility := "public"
|
|
|
|
if u.Role == "creator" {
|
|
b := GenBio(GenreForArtist(0)[0].Name)
|
|
bio = &b
|
|
t := strings.Split(b, ".")[0]
|
|
tagline = &t
|
|
w := fmt.Sprintf("https://%s.com", u.Username)
|
|
website = &w
|
|
bn := fmt.Sprintf("https://cdn.veza.music/banners/%s.jpg", u.ID)
|
|
bannerURL = &bn
|
|
}
|
|
av := fmt.Sprintf("https://cdn.veza.music/avatars/%s.jpg", u.ID)
|
|
avatarURL = &av
|
|
|
|
profileRows = append(profileRows, []interface{}{
|
|
newUUID(), u.ID, bio, tagline, nil, website, nil, nil,
|
|
avatarURL, bannerURL,
|
|
language, "UTC", theme, visibility,
|
|
false, true,
|
|
0, 0, 0, 0, // counts will be updated
|
|
u.CreatedAt, u.CreatedAt,
|
|
})
|
|
}
|
|
_, err = BulkInsert(db, "user_profiles",
|
|
"id, user_id, bio, tagline, location, website_url, birthdate, gender, avatar_url, banner_url, language, timezone, theme, profile_visibility, show_email, show_location, follower_count, following_count, track_count, playlist_count, created_at, updated_at",
|
|
profileRows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert profiles: %w", err)
|
|
}
|
|
p.Update(len(users))
|
|
p.Done()
|
|
|
|
// ── 4. User settings ─────────────────────────────────────────────────────
|
|
p = NewProgress("user_settings", len(users))
|
|
settingsRows := make([][]interface{}, 0, len(users))
|
|
for _, u := range users {
|
|
settingsRows = append(settingsRows, []interface{}{
|
|
newUUID(), u.ID,
|
|
true, true, true, true, true, true, true, true,
|
|
false, true, true, false, true,
|
|
u.CreatedAt, u.CreatedAt,
|
|
})
|
|
}
|
|
_, err = BulkInsert(db, "user_settings",
|
|
"id, user_id, email_notifications, push_notifications, browser_notifications, email_on_follow, email_on_like, email_on_comment, email_on_message, email_on_mention, email_marketing, allow_search_indexing, show_activity, explicit_content, autoplay, created_at, updated_at",
|
|
settingsRows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert settings: %w", err)
|
|
}
|
|
p.Update(len(users))
|
|
p.Done()
|
|
|
|
// ── 5. User roles (via roles table) ──────────────────────────────────────
|
|
p = NewProgress("user_roles", len(users))
|
|
// First, fetch role IDs from the roles table
|
|
roleMap := make(map[string]string)
|
|
rows, err := db.Query("SELECT id, name FROM roles")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch roles: %w", err)
|
|
}
|
|
for rows.Next() {
|
|
var id, name string
|
|
_ = rows.Scan(&id, &name)
|
|
roleMap[name] = id
|
|
}
|
|
rows.Close()
|
|
|
|
roleRows := make([][]interface{}, 0, len(users))
|
|
for _, u := range users {
|
|
roleName := u.Role
|
|
roleID, ok := roleMap[roleName]
|
|
if !ok {
|
|
// Try mapping 'creator' to a role in the table
|
|
if roleName == "creator" {
|
|
if rid, ok2 := roleMap["creator"]; ok2 {
|
|
roleID = rid
|
|
} else {
|
|
continue
|
|
}
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
roleRows = append(roleRows, []interface{}{
|
|
newUUID(), u.ID, roleID, nil, roleName, true, nil, nil,
|
|
u.CreatedAt, nil, true, u.CreatedAt,
|
|
})
|
|
}
|
|
_, err = BulkInsert(db, "user_roles",
|
|
"id, user_id, role_id, assigned_by, role, verified, verified_at, verified_by, assigned_at, expires_at, is_active, created_at",
|
|
roleRows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert user_roles: %w", err)
|
|
}
|
|
p.Update(len(users))
|
|
p.Done()
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// GetArtists returns only users with role "creator".
|
|
func GetArtists(users []SeededUser) []SeededUser {
|
|
var artists []SeededUser
|
|
for _, u := range users {
|
|
if u.Role == "creator" {
|
|
artists = append(artists, u)
|
|
}
|
|
}
|
|
return artists
|
|
}
|
|
|
|
// GetListeners returns non-creator, non-admin, non-moderator users.
|
|
func GetListeners(users []SeededUser) []SeededUser {
|
|
var listeners []SeededUser
|
|
for _, u := range users {
|
|
if u.Role == "user" || u.Role == "premium" {
|
|
listeners = append(listeners, u)
|
|
}
|
|
}
|
|
return listeners
|
|
}
|
|
|
|
// GetModerators returns users with moderator role.
|
|
func GetModerators(users []SeededUser) []SeededUser {
|
|
var mods []SeededUser
|
|
for _, u := range users {
|
|
if u.Role == "moderator" {
|
|
mods = append(mods, u)
|
|
}
|
|
}
|
|
return mods
|
|
}
|
|
|
|
// GetAdmins returns users with admin role.
|
|
func GetAdmins(users []SeededUser) []SeededUser {
|
|
var admins []SeededUser
|
|
for _, u := range users {
|
|
if u.IsAdmin {
|
|
admins = append(admins, u)
|
|
}
|
|
}
|
|
return admins
|
|
}
|