Files
Agent-System/internal/auth/bootstrap.go
T
weichenandCursor 0ebcabf7df fix(auth): tolerate concurrent demo-user seed unique conflicts
Parallel NewServer bootstrap raced with goose fixed-id user rows and
sequence drift, panicking the server package on wangzong insert. Treat
unique violations as success when the demo username exists (or retry once).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 18:18:33 +08:00

183 lines
5.3 KiB
Go

package auth
import (
"context"
"errors"
"fmt"
"os"
"strings"
"chat-agent-system/internal/config"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
func isUniqueViolation(err error) bool {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return true
}
msg := err.Error()
return strings.Contains(msg, "23505") || strings.Contains(msg, "duplicate key")
}
// BootstrapAdminFromConfig 确保配置的管理员账户在 PG 中存在且具备 role-admin。
func BootstrapAdminFromConfig(ctx context.Context, store AuthStore, cfg *config.Config) error {
if store == nil || cfg == nil {
return nil
}
adminUser := cfg.AdminUsername
adminPass := cfg.AdminPassword
if adminUser == "" {
adminUser = "admin"
}
if adminPass == "" {
return nil
}
existing, err := store.GetUserByUsername(ctx, adminUser)
if err == nil && existing != nil {
if existing.RoleID != "role-admin" {
if _, roleErr := store.UpdateUserRole(ctx, existing.ID, "role-admin", "系统管理员"); roleErr != nil {
return fmt.Errorf("bootstrap admin UpdateUserRole: %w", roleErr)
}
}
if existing.PasswordHash == "" {
hash, hashErr := HashPassword(adminPass)
if hashErr != nil {
return hashErr
}
_, err = store.UpdateUserPassword(ctx, existing.ID, hash, false)
return err
}
return nil
}
hash, err := HashPassword(adminPass)
if err != nil {
return fmt.Errorf("bootstrap admin hash: %w", err)
}
_, err = store.CreateUser(ctx, &UserAccount{
Username: adminUser,
Name: "系统管理员",
PasswordHash: hash,
Status: StatusActive,
MustChangePassword: false,
RoleID: "role-admin",
Position: "系统管理员",
Department: "系统运维部",
EmployeeID: "SYS-000",
})
return err
}
// BootstrapDevSeedUsers 在 dev/test 环境补齐演示账号(与 MemoryStore 种子对齐)。
func BootstrapDevSeedUsers(ctx context.Context, store AuthStore, cfg *config.Config) error {
if store == nil || cfg == nil {
return nil
}
if os.Getenv("SEED_DEMO_USERS") == "false" || os.Getenv("DISABLE_DEV_SEED_USERS") == "true" {
return nil
}
if cfg.Env != "dev" && cfg.Env != "test" && cfg.Env != "" {
return nil
}
seeds := []struct {
username string
name string
roleID string
position string
dept string
empID string
}{
{"zhanggong", "张工", "role-tech-lead", "技术主管", "骨科耗材研发组", "RD-001"},
{"lizhuguan", "李主管", "role-tech-lead", "技术主管", "骨科耗材研发组", "RD-002"},
{"wangzong", "王总监", "role-director", "研发总监", "研发中心", "RD-003"},
{"guest", "访客", "role-guest", "待指派", "外协中心", "EXT-999"},
}
passHash, err := HashPassword("123456")
if err != nil {
return err
}
for _, su := range seeds {
if _, err := store.GetUserByUsername(ctx, su.username); err == nil {
continue
}
_, err := store.CreateUser(ctx, &UserAccount{
Username: su.username,
Name: su.name,
PasswordHash: passHash,
Status: StatusActive,
RoleID: su.roleID,
Position: su.position,
Department: su.dept,
EmployeeID: su.empID,
})
if err != nil {
// Concurrent NewServer bootstrap and sequence drift after fixed-id
// goose seeds can race on users_pkey/username. If the demo user
// exists afterwards, treat the seed as successful.
if _, getErr := store.GetUserByUsername(ctx, su.username); getErr == nil {
continue
}
if isUniqueViolation(err) {
if _, retryErr := store.CreateUser(ctx, &UserAccount{
Username: su.username,
Name: su.name,
PasswordHash: passHash,
Status: StatusActive,
RoleID: su.roleID,
Position: su.position,
Department: su.dept,
EmployeeID: su.empID,
}); retryErr == nil {
continue
} else if _, getErr := store.GetUserByUsername(ctx, su.username); getErr == nil {
continue
}
}
return fmt.Errorf("seed user %s: %w", su.username, err)
}
}
return nil
}
// ReactivateDevSeedUsers 将 dev/test 演示账号恢复为 active(集成测试可能临时停用)。
func ReactivateDevSeedUsers(ctx context.Context, store AuthStore, cfg *config.Config) error {
if store == nil || cfg == nil {
return nil
}
if os.Getenv("SEED_DEMO_USERS") == "false" || os.Getenv("DISABLE_DEV_SEED_USERS") == "true" {
return nil
}
if cfg.Env != "dev" && cfg.Env != "test" && cfg.Env != "" {
return nil
}
for _, username := range []string{"zhanggong", "lizhuguan", "wangzong", "guest"} {
user, err := store.GetUserByUsername(ctx, username)
if err != nil || user == nil {
continue
}
if user.Status != StatusActive {
if _, err := store.UpdateUserStatus(ctx, user.ID, StatusActive); err != nil {
return fmt.Errorf("reactivate %s: %w", username, err)
}
}
}
return nil
}
// BootstrapIAM 在 PG IAM 就绪后执行管理员与 dev 种子初始化。
func BootstrapIAM(ctx context.Context, pool *pgxpool.Pool, store AuthStore, cfg *config.Config) error {
if !IAMTablesReady(ctx, pool) {
return nil
}
if err := BootstrapAdminFromConfig(ctx, store, cfg); err != nil {
return err
}
if err := BootstrapDevSeedUsers(ctx, store, cfg); err != nil {
return err
}
return ReactivateDevSeedUsers(ctx, store, cfg)
}