package user import ( "context" "crypto/rand" "database/sql" "encoding/base64" "fmt" "os" "path/filepath" "regexp" "strings" "time" ) var usernameRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9_-]{0,62}[a-z0-9])?$`) type Manager struct { db *sql.DB deployRoot string releaseRoot string } func NewManager(db *sql.DB, deployRoot, releaseRoot string) *Manager { return &Manager{ db: db, deployRoot: deployRoot, releaseRoot: releaseRoot, } } func (m *Manager) CreateOrUpdate(ctx context.Context, username string) (string, error) { if !ValidateUsername(username) { return "", fmt.Errorf("invalid username format") } token, err := generateToken() if err != nil { return "", fmt.Errorf("failed to generate token: %w", err) } now := time.Now().UTC().Format(time.RFC3339) var existingID int err = m.db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&existingID) if err == sql.ErrNoRows { _, err = m.db.ExecContext(ctx, "INSERT INTO users (username, token, created_at, updated_at) VALUES (?, ?, ?, ?)", username, token, now, now) if err != nil { return "", fmt.Errorf("failed to create user: %w", err) } } else if err == nil { _, err = m.db.ExecContext(ctx, "UPDATE users SET token = ?, updated_at = ? WHERE username = ?", token, now, username) if err != nil { return "", fmt.Errorf("failed to update user: %w", err) } } else { return "", fmt.Errorf("failed to check user existence: %w", err) } if err := m.ensureUserDirectories(username); err != nil { return "", fmt.Errorf("failed to create user directories: %w", err) } return token, nil } func (m *Manager) Delete(ctx context.Context, username string, deleteFiles bool) error { if !ValidateUsername(username) { return fmt.Errorf("invalid username format") } result, err := m.db.ExecContext(ctx, "DELETE FROM users WHERE username = ?", username) if err != nil { return fmt.Errorf("failed to delete user: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to check deletion: %w", err) } if rowsAffected == 0 { return fmt.Errorf("user not found") } if deleteFiles { deployPath := filepath.Join(m.deployRoot, username) releasePath := filepath.Join(m.releaseRoot, username) if err := os.RemoveAll(deployPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove deploy directory: %w", err) } if err := os.RemoveAll(releasePath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove release directory: %w", err) } } return nil } func (m *Manager) GetByToken(ctx context.Context, token string) (string, error) { var username string err := m.db.QueryRowContext(ctx, "SELECT username FROM users WHERE token = ?", token).Scan(&username) if err == sql.ErrNoRows { return "", fmt.Errorf("invalid token") } if err != nil { return "", fmt.Errorf("failed to query user: %w", err) } return username, nil } func (m *Manager) Exists(ctx context.Context, username string) (bool, error) { var count int err := m.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = ?", username).Scan(&count) if err != nil { return false, fmt.Errorf("failed to check user existence: %w", err) } return count > 0, nil } func (m *Manager) ensureUserDirectories(username string) error { deployPath := filepath.Join(m.deployRoot, username) releasePath := filepath.Join(m.releaseRoot, username) if err := os.MkdirAll(deployPath, 0755); err != nil { return fmt.Errorf("failed to create deploy directory: %w", err) } if err := os.MkdirAll(releasePath, 0755); err != nil { return fmt.Errorf("failed to create release directory: %w", err) } return nil } func ValidateUsername(username string) bool { return usernameRegex.MatchString(username) } func ValidateProjectName(project string) bool { return usernameRegex.MatchString(project) } func generateToken() (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return "", err } encoded := base64.URLEncoding.EncodeToString(b) alphanumeric := strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { return r } return -1 }, encoded) if len(alphanumeric) < 32 { for len(alphanumeric) < 32 { extra := make([]byte, 8) rand.Read(extra) extraEncoded := base64.URLEncoding.EncodeToString(extra) extraAlphanumeric := strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { return r } return -1 }, extraEncoded) alphanumeric += extraAlphanumeric } } return alphanumeric[:32], nil }