move auth commands to subcommands underneath auth
add tokens stored on the local device for persisting auth
This commit is contained in:
parent
318440ac95
commit
b2c7dcae83
@ -4,3 +4,4 @@ DB_NAME=termbox
|
||||
DB_USER=your_username
|
||||
DB_PASSWORD=your_password
|
||||
RESEND_API_KEY=your_resend_api_key
|
||||
JWT_SECRET=some_long_string_here
|
||||
|
||||
3
go.mod
3
go.mod
@ -3,12 +3,14 @@ module git.keircn.com/keiran/termbox
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
github.com/resend/resend-go/v2 v2.22.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/term v0.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -26,7 +28,6 @@ require (
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sync v0.13.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/time v0.5.0 // indirect
|
||||
)
|
||||
|
||||
2
go.sum
2
go.sum
@ -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/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/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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@ -16,16 +17,26 @@ import (
|
||||
|
||||
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. You'll need to verify your email.",
|
||||
Long: "Register a new account with username, email, and password. Includes email verification.",
|
||||
Run: runRegister,
|
||||
}
|
||||
|
||||
@ -36,23 +47,66 @@ var loginCmd = &cobra.Command{
|
||||
Run: runLogin,
|
||||
}
|
||||
|
||||
var verifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify your email address",
|
||||
Long: "Verify your email address with the code sent to your email.",
|
||||
Run: runVerify,
|
||||
var logoutCmd = &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Logout from your account",
|
||||
Long: "Clear stored authentication token.",
|
||||
Run: runLogout,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(registerCmd)
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
rootCmd.AddCommand(verifyCmd)
|
||||
authCmd.AddCommand(registerCmd)
|
||||
authCmd.AddCommand(loginCmd)
|
||||
authCmd.AddCommand(logoutCmd)
|
||||
rootCmd.AddCommand(authCmd)
|
||||
}
|
||||
|
||||
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 runRegister(cmd *cobra.Command, args []string) {
|
||||
fmt.Print("Username: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
@ -89,42 +143,34 @@ func runRegister(cmd *cobra.Command, args []string) {
|
||||
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.Println("Run 'termbox verify' to complete registration.")
|
||||
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 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: ")
|
||||
code, _ := reader.ReadString('\n')
|
||||
code = strings.TrimSpace(code)
|
||||
|
||||
requestBody, _ := json.Marshal(map[string]string{
|
||||
"email": email,
|
||||
"code": code,
|
||||
})
|
||||
|
||||
resp, err := http.Post(apiURL+"/auth/verify", "application/json", bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
fmt.Printf("Error verifying: %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("✅ Account verified successfully! You can now login.")
|
||||
} else {
|
||||
fmt.Printf("❌ Verification failed: %s\n", response["error"])
|
||||
fmt.Printf("Registration failed: %s\n", response["error"])
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,8 +206,24 @@ func runLogin(cmd *cobra.Command, args []string) {
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
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 {
|
||||
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.")
|
||||
}
|
||||
|
||||
@ -2,10 +2,13 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.keircn.com/keiran/termbox/internal/db"
|
||||
"git.keircn.com/keiran/termbox/internal/email"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@ -15,6 +18,17 @@ func InitEmailService() {
|
||||
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 {
|
||||
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"})
|
||||
}
|
||||
|
||||
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{
|
||||
"message": "Login successful",
|
||||
"user": user,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user