From bf913012ac61e6008ab71c282d2cadd0f672916f Mon Sep 17 00:00:00 2001 From: Kristjan Komlosi Date: Wed, 14 Jan 2026 13:44:26 +0100 Subject: [PATCH] feat: replace placeholder auth with db hash --- cmd/server/main.go | 1 + internal/handler/handlers.go | 72 +++++++++++++++++++++++++++--- migrations/001_add_users_table.sql | 22 +++++++++ 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 migrations/001_add_users_table.sql diff --git a/cmd/server/main.go b/cmd/server/main.go index e611190..98e761f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -126,6 +126,7 @@ func main() { r.GET("/health", h.Health) r.GET("/hello", h.Hello) r.POST("/login", h.Login) + r.POST("/register", h.Register) r.GET("/devices", h.GetDevices) // mqttping endpoint handled by internal/handler (protected) diff --git a/internal/handler/handlers.go b/internal/handler/handlers.go index 6bb77d1..c419631 100644 --- a/internal/handler/handlers.go +++ b/internal/handler/handlers.go @@ -14,6 +14,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" ) type Handler struct { @@ -93,7 +94,7 @@ func (h *Handler) Hello(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "hello from lambdaiot"}) } -// Login issues a JWT token for demo purposes +// Login issues a JWT token after validating credentials against the database func (h *Handler) Login(c *gin.Context) { var req struct { Username string `json:"username"` @@ -103,15 +104,31 @@ func (h *Handler) Login(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"}) return } - // demo credentials - if req.Username != "admin" || req.Password != "password" { + + // Query user from database + var userID string + var passwordHash string + err := h.DB.QueryRow("SELECT BIN_TO_UUID(id), password_hash FROM users WHERE username = ?", req.Username).Scan(&userID, &passwordHash) + if err == sql.ErrNoRows { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } - // create token + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + return + } + + // Verify password + if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) + return + } + + // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": req.Username, - "exp": time.Now().Add(time.Hour).Unix(), + "sub": req.Username, + "user_id": userID, + "exp": time.Now().Add(24 * time.Hour).Unix(), }) signed, err := token.SignedString([]byte(h.JWTSecret)) if err != nil { @@ -841,3 +858,46 @@ func (h *Handler) DeleteSensorReading(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"message": "sensor reading deleted"}) } + +// Register creates a new user account +func (h *Handler) Register(c *gin.Context) { + var req struct { + Username string `json:"username" binding:"required,min=3,max=50"` + Password string `json:"password" binding:"required,min=6"` + Email string `json:"email"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Check if username already exists + var exists int + err := h.DB.QueryRow("SELECT COUNT(*) FROM users WHERE username = ?", req.Username).Scan(&exists) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + return + } + if exists > 0 { + c.JSON(http.StatusConflict, gin.H{"error": "username already exists"}) + return + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"}) + return + } + + // Insert user + userID := uuid.New() + _, err = h.DB.Exec("INSERT INTO users (id, username, password_hash, email) VALUES (UUID_TO_BIN(?), ?, ?, ?)", + userID.String(), req.Username, string(hashedPassword), req.Email) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"id": userID.String(), "username": req.Username}) +} diff --git a/migrations/001_add_users_table.sql b/migrations/001_add_users_table.sql new file mode 100644 index 0000000..d929bd0 --- /dev/null +++ b/migrations/001_add_users_table.sql @@ -0,0 +1,22 @@ +-- Users table for authentication +CREATE TABLE users ( + id BINARY(16) NOT NULL PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + email VARCHAR(255), + created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Create index on username for faster lookups +CREATE INDEX idx_username ON users(username); + +-- Insert default admin user (password: password) +-- bcrypt hash of "password" +INSERT INTO users (id, username, password_hash, email) +VALUES ( + UUID_TO_BIN(UUID()), + 'admin', + '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', + 'admin@example.com' +);