183 lines
4.6 KiB
Go
183 lines
4.6 KiB
Go
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
|
|
}
|
|
|