termbox/internal/cli/root.go

511 lines
12 KiB
Go

package cli
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/spf13/cobra"
"golang.org/x/term"
)
const apiURL = "http://localhost:8080"
type Config struct {
Token string `json:"token"`
}
var rootCmd = &cobra.Command{
Use: "termbox",
Short: "Termbox CLI - A secure messaging platform",
Long: "Termbox CLI allows you to register, login, and send messages securely.",
}
var authCmd = &cobra.Command{
Use: "auth",
Short: "Authentication commands",
Long: "Commands for user authentication including register, login, and logout.",
}
var registerCmd = &cobra.Command{
Use: "register",
Short: "Register a new account",
Long: "Register a new account with username, email, and password. Includes email verification.",
Run: runRegister,
}
var loginCmd = &cobra.Command{
Use: "login",
Short: "Login to your account",
Long: "Login to your account with username and password.",
Run: runLogin,
}
var logoutCmd = &cobra.Command{
Use: "logout",
Short: "Logout from your account",
Long: "Clear stored authentication token.",
Run: runLogout,
}
var inboxCmd = &cobra.Command{
Use: "inbox",
Short: "View your termail inbox",
Long: "View, search, and manage your termail inbox.",
Run: runInbox,
}
var inboxViewCmd = &cobra.Command{
Use: "view [termail_id]",
Short: "View termail content",
Long: "View the full content of a specific termail by ID.",
Args: cobra.ExactArgs(1),
Run: runInboxView,
}
var inboxReadCmd = &cobra.Command{
Use: "read [termail_id]",
Short: "Mark termail as read",
Long: "Mark a specific termail as read by ID.",
Args: cobra.ExactArgs(1),
Run: runInboxRead,
}
var inboxDeleteCmd = &cobra.Command{
Use: "delete [termail_id]",
Short: "Delete termail",
Long: "Delete a specific termail by ID.",
Args: cobra.ExactArgs(1),
Run: runInboxDelete,
}
var sendCmd = &cobra.Command{
Use: "send [username]",
Short: "Send termail to a user",
Long: "Send termail to a specific user by username.",
Args: cobra.ExactArgs(1),
Run: runSend,
}
func init() {
inboxCmd.Flags().StringP("search", "s", "", "Search termails by content, subject, or sender")
inboxCmd.Flags().IntP("limit", "l", 10, "Number of termails to show")
inboxCmd.Flags().IntP("offset", "o", 0, "Number of termails to skip")
inboxCmd.Flags().Bool("unread", false, "Show only unread termails")
inboxCmd.AddCommand(inboxViewCmd)
inboxCmd.AddCommand(inboxReadCmd)
inboxCmd.AddCommand(inboxDeleteCmd)
authCmd.AddCommand(registerCmd)
authCmd.AddCommand(loginCmd)
authCmd.AddCommand(logoutCmd)
rootCmd.AddCommand(authCmd)
rootCmd.AddCommand(inboxCmd)
rootCmd.AddCommand(sendCmd)
}
func Execute() error {
return rootCmd.Execute()
}
func getConfigPath() string {
homeDir, _ := os.UserHomeDir()
return filepath.Join(homeDir, ".termbox", "config.json")
}
func loadConfig() (*Config, error) {
configPath := getConfigPath()
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return &Config{}, nil
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}
var config Config
err = json.Unmarshal(data, &config)
if err != nil {
return nil, err
}
return &config, nil
}
func saveConfig(config *Config) error {
configPath := getConfigPath()
configDir := filepath.Dir(configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
return os.WriteFile(configPath, data, 0600)
}
func makeAuthenticatedRequest(method, endpoint string, body []byte) (*http.Response, error) {
config, err := loadConfig()
if err != nil {
return nil, fmt.Errorf("failed to load config: %v", err)
}
if config.Token == "" {
return nil, fmt.Errorf("not logged in. Please run 'termbox auth login' first")
}
req, err := http.NewRequest(method, apiURL+endpoint, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+config.Token)
return http.DefaultClient.Do(req)
}
func runInboxView(cmd *cobra.Command, args []string) {
termailID, err := strconv.Atoi(args[0])
if err != nil {
fmt.Printf("Invalid termail ID: %s\n", args[0])
return
}
endpoint := fmt.Sprintf("/termail/%d", termailID)
resp, err := makeAuthenticatedRequest("GET", endpoint, nil)
if err != nil {
fmt.Printf("Error fetching termail: %v\n", err)
return
}
defer resp.Body.Close()
var response map[string]interface{}
json.NewDecoder(resp.Body).Decode(&response)
if resp.StatusCode != http.StatusOK {
fmt.Printf("Error: %s\n", response["error"])
return
}
termailData := response["termail"].(map[string]interface{})
content := fmt.Sprintf("From: %s\nSubject: %s\nSent: %s\nRead: %t\n\n%s\n",
termailData["sender"].(string),
termailData["subject"].(string),
termailData["sent_at"].(string),
termailData["is_read"].(bool),
termailData["content"].(string),
)
if shouldUsePager(content) {
if err := displayWithLess(content); err != nil {
fmt.Print(content)
}
} else {
fmt.Print(content)
}
markAsReadEndpoint := fmt.Sprintf("/termail/%d/read", termailID)
makeAuthenticatedRequest("POST", markAsReadEndpoint, nil)
}
func shouldUsePager(content string) bool {
lines := strings.Count(content, "\n")
return lines > 20
}
func displayWithLess(content string) error {
lessCmd := exec.Command("less", "-R")
lessCmd.Stdin = strings.NewReader(content)
lessCmd.Stdout = os.Stdout
lessCmd.Stderr = os.Stderr
return lessCmd.Run()
}
func runInbox(cmd *cobra.Command, args []string) {
search, _ := cmd.Flags().GetString("search")
limit, _ := cmd.Flags().GetInt("limit")
offset, _ := cmd.Flags().GetInt("offset")
unread, _ := cmd.Flags().GetBool("unread")
endpoint := fmt.Sprintf("/termail/inbox?limit=%d&offset=%d", limit, offset)
if search != "" {
endpoint += "&search=" + search
}
if unread {
endpoint += "&unread=true"
}
resp, err := makeAuthenticatedRequest("GET", endpoint, nil)
if err != nil {
fmt.Printf("Error fetching inbox: %v\n", err)
return
}
defer resp.Body.Close()
var response map[string]interface{}
json.NewDecoder(resp.Body).Decode(&response)
if resp.StatusCode == http.StatusOK {
termails, exists := response["termails"]
if !exists || termails == nil {
fmt.Println("No termails found.")
return
}
termailList, ok := termails.([]interface{})
if !ok {
fmt.Println("No termails found.")
return
}
if len(termailList) == 0 {
fmt.Println("No termails found.")
return
}
for _, t := range termailList {
termail := t.(map[string]interface{})
status := "READ"
if isRead, ok := termail["is_read"].(bool); ok && !isRead {
status = "UNREAD"
}
fmt.Printf("[%s] ID: %.0f | From: %s | Subject: %s | Sent: %s\n",
status,
termail["id"].(float64),
termail["sender"].(string),
termail["subject"].(string),
termail["sent_at"].(string),
)
}
} else {
fmt.Printf("Error: %s\n", response["error"])
}
}
func runInboxRead(cmd *cobra.Command, args []string) {
termailID, err := strconv.Atoi(args[0])
if err != nil {
fmt.Printf("Invalid termail ID: %s\n", args[0])
return
}
endpoint := fmt.Sprintf("/termail/%d/read", termailID)
resp, err := makeAuthenticatedRequest("POST", endpoint, nil)
if err != nil {
fmt.Printf("Error marking termail as read: %v\n", err)
return
}
defer resp.Body.Close()
var response map[string]interface{}
json.NewDecoder(resp.Body).Decode(&response)
if resp.StatusCode == http.StatusOK {
fmt.Println("Termail marked as read.")
} else {
fmt.Printf("Error: %s\n", response["error"])
}
}
func runInboxDelete(cmd *cobra.Command, args []string) {
termailID, err := strconv.Atoi(args[0])
if err != nil {
fmt.Printf("Invalid termail ID: %s\n", args[0])
return
}
endpoint := fmt.Sprintf("/termail/%d", termailID)
resp, err := makeAuthenticatedRequest("DELETE", endpoint, nil)
if err != nil {
fmt.Printf("Error deleting termail: %v\n", err)
return
}
defer resp.Body.Close()
var response map[string]interface{}
json.NewDecoder(resp.Body).Decode(&response)
if resp.StatusCode == http.StatusOK {
fmt.Println("Termail deleted.")
} else {
fmt.Printf("Error: %s\n", response["error"])
}
}
func runSend(cmd *cobra.Command, args []string) {
receiverUsername := args[0]
fmt.Print("Subject: ")
reader := bufio.NewReader(os.Stdin)
subject, _ := reader.ReadString('\n')
subject = strings.TrimSpace(subject)
fmt.Println("Content (press Ctrl+D when finished):")
var contentLines []string
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
contentLines = append(contentLines, scanner.Text())
}
content := strings.Join(contentLines, "\n")
requestBody, _ := json.Marshal(map[string]string{
"receiver_username": receiverUsername,
"subject": subject,
"content": content,
})
resp, err := makeAuthenticatedRequest("POST", "/termail/send", requestBody)
if err != nil {
fmt.Printf("Error sending termail: %v\n", err)
return
}
defer resp.Body.Close()
var response map[string]interface{}
json.NewDecoder(resp.Body).Decode(&response)
if resp.StatusCode == http.StatusCreated {
fmt.Printf("Termail sent successfully to %s!\n", receiverUsername)
} else {
fmt.Printf("Error: %s\n", response["error"])
}
}
func runRegister(cmd *cobra.Command, args []string) {
fmt.Print("Username: ")
reader := bufio.NewReader(os.Stdin)
username, _ := reader.ReadString('\n')
username = strings.TrimSpace(username)
fmt.Print("Email: ")
email, _ := reader.ReadString('\n')
email = strings.TrimSpace(email)
fmt.Print("Password: ")
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
fmt.Printf("Error reading password: %v\n", err)
return
}
password := string(passwordBytes)
fmt.Println()
requestBody, _ := json.Marshal(map[string]string{
"username": username,
"email": email,
"password": password,
})
resp, err := http.Post(apiURL+"/auth/register", "application/json", bytes.NewBuffer(requestBody))
if err != nil {
fmt.Printf("Error registering: %v\n", err)
return
}
defer resp.Body.Close()
var response map[string]interface{}
json.NewDecoder(resp.Body).Decode(&response)
if resp.StatusCode == http.StatusCreated {
fmt.Printf("Registration successful! Check your email (%s) for verification code.\n", email)
fmt.Print("Verification code: ")
code, _ := reader.ReadString('\n')
code = strings.TrimSpace(code)
verifyBody, _ := json.Marshal(map[string]string{
"email": email,
"code": code,
})
verifyResp, err := http.Post(apiURL+"/auth/verify", "application/json", bytes.NewBuffer(verifyBody))
if err != nil {
fmt.Printf("Error verifying: %v\n", err)
return
}
defer verifyResp.Body.Close()
var verifyResponse map[string]interface{}
json.NewDecoder(verifyResp.Body).Decode(&verifyResponse)
if verifyResp.StatusCode == http.StatusOK {
fmt.Println("Account verified successfully! You can now login.")
} else {
fmt.Printf("Verification failed: %s\n", verifyResponse["error"])
}
} else {
fmt.Printf("Registration failed: %s\n", response["error"])
}
}
func runLogin(cmd *cobra.Command, args []string) {
fmt.Print("Username: ")
reader := bufio.NewReader(os.Stdin)
username, _ := reader.ReadString('\n')
username = strings.TrimSpace(username)
fmt.Print("Password: ")
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
fmt.Printf("Error reading password: %v\n", err)
return
}
password := string(passwordBytes)
fmt.Println()
requestBody, _ := json.Marshal(map[string]string{
"username": username,
"password": password,
})
resp, err := http.Post(apiURL+"/auth/login", "application/json", bytes.NewBuffer(requestBody))
if err != nil {
fmt.Printf("Error logging in: %v\n", err)
return
}
defer resp.Body.Close()
var response map[string]interface{}
json.NewDecoder(resp.Body).Decode(&response)
if resp.StatusCode == http.StatusOK {
user := response["user"].(map[string]interface{})
token := response["token"].(string)
config := &Config{Token: token}
if err := saveConfig(config); err != nil {
fmt.Printf("Warning: Failed to save login token: %v\n", err)
}
fmt.Printf("Login successful! Welcome back, %s!\n", user["username"])
} else {
fmt.Printf("Login failed: %s\n", response["error"])
}
}
func runLogout(cmd *cobra.Command, args []string) {
config := &Config{}
if err := saveConfig(config); err != nil {
fmt.Printf("Error clearing token: %v\n", err)
return
}
fmt.Println("Logged out successfully.")
}