move auth commands to subcommands underneath auth

add tokens stored on the local device for persisting auth
This commit is contained in:
Keiran 2025-08-06 06:26:42 +01:00
parent 318440ac95
commit b2c7dcae83
5 changed files with 134 additions and 48 deletions

View File

@ -4,3 +4,4 @@ DB_NAME=termbox
DB_USER=your_username DB_USER=your_username
DB_PASSWORD=your_password DB_PASSWORD=your_password
RESEND_API_KEY=your_resend_api_key RESEND_API_KEY=your_resend_api_key
JWT_SECRET=some_long_string_here

3
go.mod
View File

@ -3,12 +3,14 @@ module git.keircn.com/keiran/termbox
go 1.24.5 go 1.24.5
require ( require (
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/jackc/pgx/v5 v5.7.5 github.com/jackc/pgx/v5 v5.7.5
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.12.0 github.com/labstack/echo/v4 v4.12.0
github.com/resend/resend-go/v2 v2.22.0 github.com/resend/resend-go/v2 v2.22.0
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.37.0
golang.org/x/term v0.31.0
) )
require ( require (
@ -26,7 +28,6 @@ require (
golang.org/x/net v0.24.0 // indirect golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.13.0 // indirect golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
) )

2
go.sum
View File

@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings" "strings"
"syscall" "syscall"
@ -16,16 +17,26 @@ import (
const apiURL = "http://localhost:8080" const apiURL = "http://localhost:8080"
type Config struct {
Token string `json:"token"`
}
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "termbox", Use: "termbox",
Short: "Termbox CLI - A secure messaging platform", Short: "Termbox CLI - A secure messaging platform",
Long: "Termbox CLI allows you to register, login, and send messages securely.", 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{ var registerCmd = &cobra.Command{
Use: "register", Use: "register",
Short: "Register a new account", Short: "Register a new account",
Long: "Register a new account with username, email, and password. You'll need to verify your email.", Long: "Register a new account with username, email, and password. Includes email verification.",
Run: runRegister, Run: runRegister,
} }
@ -36,23 +47,66 @@ var loginCmd = &cobra.Command{
Run: runLogin, Run: runLogin,
} }
var verifyCmd = &cobra.Command{ var logoutCmd = &cobra.Command{
Use: "verify", Use: "logout",
Short: "Verify your email address", Short: "Logout from your account",
Long: "Verify your email address with the code sent to your email.", Long: "Clear stored authentication token.",
Run: runVerify, Run: runLogout,
} }
func init() { func init() {
rootCmd.AddCommand(registerCmd) authCmd.AddCommand(registerCmd)
rootCmd.AddCommand(loginCmd) authCmd.AddCommand(loginCmd)
rootCmd.AddCommand(verifyCmd) authCmd.AddCommand(logoutCmd)
rootCmd.AddCommand(authCmd)
} }
func Execute() error { func Execute() error {
return rootCmd.Execute() 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 runRegister(cmd *cobra.Command, args []string) { func runRegister(cmd *cobra.Command, args []string) {
fmt.Print("Username: ") fmt.Print("Username: ")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@ -89,42 +143,34 @@ func runRegister(cmd *cobra.Command, args []string) {
json.NewDecoder(resp.Body).Decode(&response) json.NewDecoder(resp.Body).Decode(&response)
if resp.StatusCode == http.StatusCreated { if resp.StatusCode == http.StatusCreated {
fmt.Printf("✅ Registration successful! Check your email (%s) for verification code.\n", email) fmt.Printf("Registration successful! Check your email (%s) for verification code.\n", email)
fmt.Println("Run 'termbox verify' to complete registration.")
} else {
fmt.Printf("❌ Registration failed: %s\n", response["error"])
}
}
func runVerify(cmd *cobra.Command, args []string) {
fmt.Print("Email: ")
reader := bufio.NewReader(os.Stdin)
email, _ := reader.ReadString('\n')
email = strings.TrimSpace(email)
fmt.Print("Verification code: ") fmt.Print("Verification code: ")
code, _ := reader.ReadString('\n') code, _ := reader.ReadString('\n')
code = strings.TrimSpace(code) code = strings.TrimSpace(code)
requestBody, _ := json.Marshal(map[string]string{ verifyBody, _ := json.Marshal(map[string]string{
"email": email, "email": email,
"code": code, "code": code,
}) })
resp, err := http.Post(apiURL+"/auth/verify", "application/json", bytes.NewBuffer(requestBody)) verifyResp, err := http.Post(apiURL+"/auth/verify", "application/json", bytes.NewBuffer(verifyBody))
if err != nil { if err != nil {
fmt.Printf("Error verifying: %v\n", err) fmt.Printf("Error verifying: %v\n", err)
return return
} }
defer resp.Body.Close() defer verifyResp.Body.Close()
var response map[string]interface{} var verifyResponse map[string]interface{}
json.NewDecoder(resp.Body).Decode(&response) json.NewDecoder(verifyResp.Body).Decode(&verifyResponse)
if resp.StatusCode == http.StatusOK { if verifyResp.StatusCode == http.StatusOK {
fmt.Println("Account verified successfully! You can now login.") fmt.Println("Account verified successfully! You can now login.")
} else { } else {
fmt.Printf("❌ Verification failed: %s\n", response["error"]) fmt.Printf("Verification failed: %s\n", verifyResponse["error"])
}
} else {
fmt.Printf("Registration failed: %s\n", response["error"])
} }
} }
@ -160,8 +206,24 @@ func runLogin(cmd *cobra.Command, args []string) {
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
user := response["user"].(map[string]interface{}) user := response["user"].(map[string]interface{})
fmt.Printf("✅ Login successful! Welcome back, %s!\n", user["username"]) 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 { } else {
fmt.Printf("❌ Login failed: %s\n", response["error"]) 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.")
}

View File

@ -2,10 +2,13 @@ package handlers
import ( import (
"net/http" "net/http"
"os"
"strings" "strings"
"time"
"git.keircn.com/keiran/termbox/internal/db" "git.keircn.com/keiran/termbox/internal/db"
"git.keircn.com/keiran/termbox/internal/email" "git.keircn.com/keiran/termbox/internal/email"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -15,6 +18,17 @@ func InitEmailService() {
emailService = email.NewService() emailService = email.NewService()
} }
func generateToken(userID int, username string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"username": username,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("JWT_SECRET")))
}
func HandleRoot(c echo.Context) error { func HandleRoot(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!") return c.String(http.StatusOK, "Hello, World!")
} }
@ -90,8 +104,14 @@ func HandleLogin(c echo.Context) error {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials or account not verified"}) return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials or account not verified"})
} }
token, err := generateToken(user.ID, user.Username)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to generate token"})
}
return c.JSON(http.StatusOK, map[string]any{ return c.JSON(http.StatusOK, map[string]any{
"message": "Login successful", "message": "Login successful",
"user": user, "user": user,
"token": token,
}) })
} }