Files
tingz/internal/user/manager.go
2025-10-14 21:20:25 +03:00

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
}