From 2dc87a84b998795f51375c2219e29ca9083e0e44 Mon Sep 17 00:00:00 2001 From: mahesh bhatiya Date: Thu, 19 Jun 2025 20:51:26 +0530 Subject: [PATCH] feat(remove): add domain teardown command with user and database cleanup - Introduced `remove-domain` command to fully remove a domain setup. - Disables and deletes server configuration (Apache/Nginx/Caddy). - Removes MySQL user and database using internal SQL executor. - Deletes Linux shell user and home directory unless `--keep-user` is set. - Includes safe checks to avoid deleting current logged-in user. - Adds modular support via `internal/mysql_drop.go`. --- cmd/remove.go | 108 +++++++++++++++++++++++++++++++++++++++++ internal/mysql_drop.go | 22 +++++++++ 2 files changed, 130 insertions(+) create mode 100644 cmd/remove.go create mode 100644 internal/mysql_drop.go diff --git a/cmd/remove.go b/cmd/remove.go new file mode 100644 index 0000000..767a599 --- /dev/null +++ b/cmd/remove.go @@ -0,0 +1,108 @@ +package cmd + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "stackroost/internal" + "stackroost/internal/logger" +) + +var removeDomainCmd = &cobra.Command{ + Use: "remove-domain", + Short: "Remove a domain configuration, user, and database", + Run: func(cmd *cobra.Command, args []string) { + domain, _ := cmd.Flags().GetString("name") + serverType, _ := cmd.Flags().GetString("server") + keepUser, _ := cmd.Flags().GetBool("keep-user") + + if internal.IsNilOrEmpty(domain) { + logger.Error("Domain name is required") + os.Exit(1) + } + + username := strings.Split(domain, ".")[0] + filename := domain + ".conf" + + logger.Info(fmt.Sprintf("Removing domain: %s", domain)) + + // Step 1: Disable web server site + switch serverType { + case "apache": + logger.Info("Disabling Apache site") + internal.RunCommand("sudo", "a2dissite", filename) + internal.RunCommand("sudo", "systemctl", "reload", "apache2") + case "nginx": + link := filepath.Join("/etc/nginx/sites-enabled", filename) + internal.RunCommand("sudo", "rm", "-f", link) + internal.RunCommand("sudo", "systemctl", "reload", "nginx") + case "caddy": + link := filepath.Join("/etc/caddy/sites-enabled", filename) + internal.RunCommand("sudo", "rm", "-f", link) + internal.RunCommand("sudo", "systemctl", "reload", "caddy") + default: + logger.Error(fmt.Sprintf("Unsupported server type: %s", serverType)) + os.Exit(1) + } + + // Step 2: Remove config file + configPath := getServerConfigPath(serverType, domain) + if err := os.Remove(configPath); err != nil { + logger.Warn(fmt.Sprintf("Could not delete config file: %v", err)) + } else { + logger.Success(fmt.Sprintf("Removed config file: %s", configPath)) + } + + // Step 3: Remove MySQL database and user + if err := internal.DropMySQLUserAndDatabase(username); err != nil { + logger.Warn(fmt.Sprintf("MySQL cleanup failed: %v", err)) + } else { + logger.Success("MySQL user and database removed") + } + + // Step 4: Remove system user + if !keepUser { + logger.Info(fmt.Sprintf("Removing Linux user: %s", username)) + + // Sanity check - prevent deleting yourself + currentUser, _ := user.Current() + if currentUser.Username == username { + logger.Error("Refusing to delete the current executing user") + os.Exit(1) + } + + internal.RunCommand("sudo", "userdel", "-r", username) + logger.Success(fmt.Sprintf("User '%s' and home directory removed", username)) + } else { + logger.Info("Keeping shell user and home directory (per flag)") + } + + logger.Success(fmt.Sprintf("Domain '%s' removed successfully", domain)) + }, +} + +func getServerConfigPath(serverType, domain string) string { + filename := domain + ".conf" + switch serverType { + case "apache": + return filepath.Join("/etc/apache2/sites-available", filename) + case "nginx": + return filepath.Join("/etc/nginx/sites-available", filename) + case "caddy": + return filepath.Join("/etc/caddy/sites-available", filename) + default: + return "" + } +} + +func init() { + rootCmd.AddCommand(removeDomainCmd) + removeDomainCmd.Flags().StringP("name", "n", "", "Domain name to remove") + removeDomainCmd.Flags().StringP("server", "s", "apache", "Server type (apache, nginx, caddy)") + removeDomainCmd.Flags().Bool("keep-user", false, "Keep the Linux user and home directory") + removeDomainCmd.MarkFlagRequired("name") +} diff --git a/internal/mysql_drop.go b/internal/mysql_drop.go new file mode 100644 index 0000000..41d309c --- /dev/null +++ b/internal/mysql_drop.go @@ -0,0 +1,22 @@ +package internal + +import ( + "fmt" + "stackroost/internal/logger" +) + +func DropMySQLUserAndDatabase(username string) error { + logger.Info(fmt.Sprintf("Dropping MySQL user and database: %s", username)) + + sql := fmt.Sprintf(` + DROP DATABASE IF EXISTS %s; + DROP USER IF EXISTS '%s'@'localhost'; + FLUSH PRIVILEGES; + `, username, username) + + cmd := []string{"-e", sql} + if err := RunCommand("sudo", append([]string{"mysql"}, cmd...)...); err != nil { + return fmt.Errorf("failed to drop MySQL user or database: %v", err) + } + return nil +}