veza/veza-backend-api/cmd/tools/seed/seed_users.go
senke a1000ce7fb style(backend): gofmt -w on 85 files (whitespace only)
backend-ci.yml's `test -z "$(gofmt -l .)"` strict gate (added in
13c21ac11) failed on a backlog of unformatted files. None of the
85 files in this commit had been edited since the gate was added
because no push touched veza-backend-api/** in between, so the
gate never fired until today's CI fixes triggered it.

The diff is exclusively whitespace alignment in struct literals
and trailing-space comments. `go build ./...` and the full test
suite (with VEZA_SKIP_INTEGRATION=1 -short) pass identically.
2026-04-14 12:22:14 +02:00

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
}