feat(server): add email verification and add a proper cli to the termbox binary

This commit is contained in:
Keiran 2025-08-06 06:15:52 +01:00
parent 24ca498f62
commit 9404620b39
12 changed files with 458 additions and 23 deletions

View File

@ -3,3 +3,4 @@ DB_PORT=5432
DB_NAME=termbox
DB_USER=your_username
DB_PASSWORD=your_password
RESEND_API_KEY=your_resend_api_key

View File

@ -2,12 +2,16 @@ BINARY = termbox
SRV_BINARY = termbox_server
GO_ENV = GOOS=linux GOARCH=amd64
.PHONY: build clean
.PHONY: build clean test run-server dev-server
all: build
build: clean
mkdir -p build
go build -o build/${BINARY} cmd/termbox/main.go
go build -o build/${SRV_BINARY} cmd/termbox_server/main.go
clean:
rm -rf build
dev:
go run cmd/termbox_server/main.go

View File

@ -1,7 +1,13 @@
package main
import "fmt"
import (
"os"
"git.keircn.com/keiran/termbox/internal/cli"
)
func main() {
fmt.Println("Hello, Termbox!")
if err := cli.Execute(); err != nil {
os.Exit(1)
}
}

View File

@ -13,6 +13,8 @@ func main() {
db.InitDB()
defer db.Close()
handlers.InitEmailService()
e := echo.New()
e.Use(middleware.Logger())
@ -21,6 +23,7 @@ func main() {
e.GET("/", handlers.HandleRoot)
e.POST("/auth/register", handlers.HandleRegister)
e.POST("/auth/verify", handlers.HandleVerifyCode)
e.POST("/auth/login", handlers.HandleLogin)
log.Fatal(e.Start(":8080"))

5
go.mod
View File

@ -6,22 +6,27 @@ require (
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
)
require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
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
)

12
go.sum
View File

@ -1,8 +1,11 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@ -24,6 +27,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/resend/resend-go/v2 v2.22.0 h1:rx52hlFeyiu01Ie5PBLJRYkde9WNEHDnrg/oGgOGDzk=
github.com/resend/resend-go/v2 v2.22.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -43,6 +53,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=

167
internal/cli/root.go Normal file
View File

@ -0,0 +1,167 @@
package cli
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"syscall"
"github.com/spf13/cobra"
"golang.org/x/term"
)
const apiURL = "http://localhost:8080"
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 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.",
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 verifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify your email address",
Long: "Verify your email address with the code sent to your email.",
Run: runVerify,
}
func init() {
rootCmd.AddCommand(registerCmd)
rootCmd.AddCommand(loginCmd)
rootCmd.AddCommand(verifyCmd)
}
func Execute() error {
return rootCmd.Execute()
}
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.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: ")
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"])
}
}
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{})
fmt.Printf("✅ Login successful! Welcome back, %s!\n", user["username"])
} else {
fmt.Printf("❌ Login failed: %s\n", response["error"])
}
}

View File

@ -57,7 +57,18 @@ func CreateTables() error {
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS verification_codes (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
code VARCHAR(6) NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
@ -72,7 +83,6 @@ func CreateTables() error {
return err
}
func Close() {
if Pool != nil {
Pool.Close()

View File

@ -1,7 +1,19 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE verification_codes (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
code VARCHAR(6) NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE messages (

View File

@ -2,6 +2,10 @@ package db
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
@ -10,12 +14,14 @@ import (
type User struct {
ID int `json:"id"`
Username string `json:"username"`
PasswordHash string `json:"-"`
Email string `json:"email"`
IsVerified bool `json:"is_verified"`
CreatedAt time.Time `json:"created_at"`
}
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
@ -24,6 +30,35 @@ type LoginRequest struct {
Password string `json:"password"`
}
type VerifyCodeRequest struct {
Email string `json:"email"`
Code string `json:"code"`
}
type VerificationCode struct {
ID int `json:"id"`
UserID int `json:"user_id"`
Code string `json:"code"`
ExpiresAt time.Time `json:"expires_at"`
Used bool `json:"used"`
CreatedAt time.Time `json:"created_at"`
}
func GenerateVerificationCode() (string, error) {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const length = 6
result := make([]byte, length)
for i := range result {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
result[i] = charset[num.Int64()]
}
return string(result), nil
}
func CreateUser(ctx context.Context, req CreateUserRequest) (*User, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
@ -32,9 +67,9 @@ func CreateUser(ctx context.Context, req CreateUserRequest) (*User, error) {
var user User
err = Pool.QueryRow(ctx,
"INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id, username, created_at",
req.Username, string(hashedPassword),
).Scan(&user.ID, &user.Username, &user.CreatedAt)
"INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING id, username, email, is_verified, created_at",
req.Username, strings.ToLower(req.Email), string(hashedPassword),
).Scan(&user.ID, &user.Username, &user.Email, &user.IsVerified, &user.CreatedAt)
if err != nil {
return nil, err
@ -43,12 +78,93 @@ func CreateUser(ctx context.Context, req CreateUserRequest) (*User, error) {
return &user, nil
}
func CreateVerificationCode(ctx context.Context, userID int) (*VerificationCode, error) {
code, err := GenerateVerificationCode()
if err != nil {
return nil, err
}
expiresAt := time.Now().Add(10 * time.Minute)
var verificationCode VerificationCode
err = Pool.QueryRow(ctx,
"INSERT INTO verification_codes (user_id, code, expires_at) VALUES ($1, $2, $3) RETURNING id, user_id, code, expires_at, used, created_at",
userID, code, expiresAt,
).Scan(&verificationCode.ID, &verificationCode.UserID, &verificationCode.Code, &verificationCode.ExpiresAt, &verificationCode.Used, &verificationCode.CreatedAt)
if err != nil {
return nil, err
}
return &verificationCode, nil
}
func VerifyCode(ctx context.Context, email, code string) error {
tx, err := Pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var userID int
var codeID int
var expiresAt time.Time
var used bool
err = tx.QueryRow(ctx, `
SELECT u.id, vc.id, vc.expires_at, vc.used
FROM users u
JOIN verification_codes vc ON u.id = vc.user_id
WHERE u.email = $1 AND vc.code = $2
ORDER BY vc.created_at DESC
LIMIT 1
`, strings.ToLower(email), code).Scan(&userID, &codeID, &expiresAt, &used)
if err != nil {
return fmt.Errorf("invalid verification code")
}
if used {
return fmt.Errorf("verification code already used")
}
if time.Now().After(expiresAt) {
return fmt.Errorf("verification code expired")
}
_, err = tx.Exec(ctx, "UPDATE verification_codes SET used = true WHERE id = $1", codeID)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE users SET is_verified = true WHERE id = $1", userID)
if err != nil {
return err
}
return tx.Commit(ctx)
}
func GetUserByUsername(ctx context.Context, username string) (*User, error) {
var user User
err := Pool.QueryRow(ctx,
"SELECT id, username, password_hash, created_at FROM users WHERE username = $1",
"SELECT id, username, email, is_verified, created_at FROM users WHERE username = $1",
username,
).Scan(&user.ID, &user.Username, &user.PasswordHash, &user.CreatedAt)
).Scan(&user.ID, &user.Username, &user.Email, &user.IsVerified, &user.CreatedAt)
if err != nil {
return nil, err
}
return &user, nil
}
func GetUserByEmail(ctx context.Context, email string) (*User, error) {
var user User
err := Pool.QueryRow(ctx,
"SELECT id, username, email, is_verified, created_at FROM users WHERE email = $1",
strings.ToLower(email),
).Scan(&user.ID, &user.Username, &user.Email, &user.IsVerified, &user.CreatedAt)
if err != nil {
return nil, err
@ -58,15 +174,25 @@ func GetUserByUsername(ctx context.Context, username string) (*User, error) {
}
func ValidateUserCredentials(ctx context.Context, username, password string) (*User, error) {
user, err := GetUserByUsername(ctx, username)
var user User
var passwordHash string
err := Pool.QueryRow(ctx,
"SELECT id, username, email, password_hash, is_verified, created_at FROM users WHERE username = $1",
username,
).Scan(&user.ID, &user.Username, &user.Email, &passwordHash, &user.IsVerified, &user.CreatedAt)
if err != nil {
return nil, err
}
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if !user.IsVerified {
return nil, fmt.Errorf("account not verified")
}
err = bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
if err != nil {
return nil, err
}
return user, nil
return &user, nil
}

50
internal/email/service.go Normal file
View File

@ -0,0 +1,50 @@
package email
import (
"context"
"fmt"
"os"
"github.com/resend/resend-go/v2"
)
type Service struct {
client *resend.Client
}
func NewService() *Service {
apiKey := os.Getenv("RESEND_API_KEY")
if apiKey == "" {
panic("RESEND_API_KEY environment variable is required")
}
client := resend.NewClient(apiKey)
return &Service{client: client}
}
func (s *Service) SendVerificationCode(ctx context.Context, email, username, code string) error {
params := &resend.SendEmailRequest{
From: "noreply@termbox.keircn.com",
To: []string{email},
Subject: "Verify your Termbox account",
Html: fmt.Sprintf(`
<h2>Welcome to Termbox, %s!</h2>
<p>Please verify your account with the following code:</p>
<h1 style="font-family: monospace; letter-spacing: 5px; color: #2563eb;">%s</h1>
<p>This code will expire in 10 minutes.</p>
<p>If you didn't create an account, please ignore this email.</p>
`, username, code),
Text: fmt.Sprintf(`
Welcome to Termbox, %s!
Please verify your account with the following code: %s
This code will expire in 10 minutes.
If you didn't create an account, please ignore this email.
`, username, code),
}
_, err := s.client.Emails.Send(params)
return err
}

View File

@ -2,11 +2,19 @@ package handlers
import (
"net/http"
"strings"
"git.keircn.com/keiran/termbox/internal/db"
"git.keircn.com/keiran/termbox/internal/email"
"github.com/labstack/echo/v4"
)
var emailService *email.Service
func InitEmailService() {
emailService = email.NewService()
}
func HandleRoot(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
@ -17,21 +25,52 @@ func HandleRegister(c echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
}
if req.Username == "" || req.Password == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Username and password are required"})
if req.Username == "" || req.Email == "" || req.Password == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Username, email, and password are required"})
}
user, err := db.CreateUser(c.Request().Context(), req)
if err != nil {
return c.JSON(http.StatusConflict, map[string]string{"error": "Username already exists"})
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
return c.JSON(http.StatusConflict, map[string]string{"error": "Username or email already exists"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to create user: " + err.Error()})
}
verificationCode, err := db.CreateVerificationCode(c.Request().Context(), user.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to create verification code"})
}
err = emailService.SendVerificationCode(c.Request().Context(), user.Email, user.Username, verificationCode.Code)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to send verification email"})
}
return c.JSON(http.StatusCreated, map[string]any{
"message": "User created successfully",
"message": "User created successfully. Please check your email for verification code.",
"user": user,
})
}
func HandleVerifyCode(c echo.Context) error {
var req db.VerifyCodeRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
}
if req.Email == "" || req.Code == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Email and code are required"})
}
err := db.VerifyCode(c.Request().Context(), req.Email, req.Code)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"message": "Account verified successfully"})
}
func HandleLogin(c echo.Context) error {
var req db.LoginRequest
if err := c.Bind(&req); err != nil {
@ -44,7 +83,7 @@ func HandleLogin(c echo.Context) error {
user, err := db.ValidateUserCredentials(c.Request().Context(), req.Username, req.Password)
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials or account not verified"})
}
return c.JSON(http.StatusOK, map[string]any{