511 lines
12 KiB
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.")
|
|
}
|