diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..a2db5da --- /dev/null +++ b/auth.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/dgrijalva/jwt-go" + "golang.org/x/crypto/bcrypt" +) + +// Define contextKey globally within the package +type contextKey string + +// Define your key as a constant of the custom type +const userKey contextKey = "user" + +// LoginRequest represents the request body for the /login endpoint. +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// LoginResponse represents the response body for the /login endpoint. +type LoginResponse struct { + Token string `json:"token"` +} + +func init() { + fmt.Printf("handlers.go init: config = %+v", config) +} + +// loginHandler handles the /login endpoint. +// LoginHandler handles the /login endpoint. +func LoginHandler(w http.ResponseWriter, r *http.Request) { + var req LoginRequest + //fmt.Println("db is ", db) + //fmt.Println("config is ", config) + if db == nil { + fmt.Println("DB is nil") + http.Error(w, "Database not initialized", http.StatusInternalServerError) + return + } + fmt.Println("DB is not nil") + + //if config == nil { + //fmt.Println("Config is nil in LoginHandler") + //h//ttp.Error(w, "Configuration not loaded", http.StatusInternalServerError) + //return + //} + //fmt.Println("Config is not nil") + + fmt.Printf("DB: %+v\n", db) + //fmt.Printf("Config: %+v\n", config) + fmt.Println("LoginHandler called") + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Check if the user exists and the password matches + var user User + db.Where("username = ?", req.Username).First(&user) + if user.ID == 0 { + http.Error(w, "Invalid username or password", http.StatusUnauthorized) + return + } + + // Compare the provided password with the stored hashed password + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)) + if err != nil { + http.Error(w, "Invalid username or password", http.StatusUnauthorized) + return + } + + // Generate JWT token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "username": user.Username, + "exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours + }) + + tokenString, err := token.SignedString(*JWTSecret) + if err != nil { + http.Error(w, "Failed to generate token", http.StatusInternalServerError) + return + } + + // Return the token in the response + json.NewEncoder(w).Encode(LoginResponse{Token: tokenString}) +} + +// authMiddleware is a middleware function that checks for a valid JWT token in the request header and enables CORS. +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") // Replace "*" with your allowed frontend origin if needed + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + // Handle preflight request for CORS + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + // Get the token from the request header + tokenString := r.Header.Get("Authorization") + if tokenString == "" { + http.Error(w, "Authorization header missing", http.StatusUnauthorized) + return + } + + // Remove "Bearer " prefix from token string + tokenString = strings.Replace(tokenString, "Bearer ", "", 1) + + // Parse and validate the JWT token + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Make sure that the signing method is HMAC + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return *JWTSecret, nil + }) + if err != nil || !token.Valid { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Extract the user claims from the token + if claims, ok := token.Claims.(jwt.MapClaims); ok { + // Add the "user" claim to the request context + newCtx := context.WithValue(r.Context(), userKey, claims["username"]) + r = r.WithContext(newCtx) + } else { + http.Error(w, "Invalid token claims", http.StatusUnauthorized) + return + } + + // Call the next handler in the chain + next.ServeHTTP(w, r) + }) +} diff --git a/boxes-api b/boxes-api deleted file mode 100755 index a8035d9..0000000 Binary files a/boxes-api and /dev/null differ diff --git a/boxes.log b/boxes.log new file mode 100644 index 0000000..680d3ad --- /dev/null +++ b/boxes.log @@ -0,0 +1,10 @@ +2024/10/26 20:47:15 main.go:94: Logging initialized +2024/10/26 20:47:15 main.go:106: Connected to database in connectToDatabase +2024/10/26 20:47:15 main.go:116: Static directory error: static directory does not exist: ./build/ +2024/10/26 20:47:52 main.go:94: Logging initialized +2024/10/26 20:47:52 main.go:106: Connected to database in connectToDatabase +2024/10/26 20:47:52 main.go:116: Static directory error: static directory does not exist: ./build/ +2024/10/26 20:49:06 main.go:94: Logging initialized +2024/10/26 20:49:06 main.go:106: Connected to database in connectToDatabase +2024/10/26 22:19:11 main.go:94: Logging initialized +2024/10/26 22:19:11 main.go:106: Connected to database in connectToDatabase diff --git a/boxes_handlers.go b/boxes_handlers.go new file mode 100644 index 0000000..92f8f6b --- /dev/null +++ b/boxes_handlers.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/mux" +) + +// getBoxesHandler handles the GET /boxes endpoint. +func GetBoxesHandler(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Received %s request to %s\n", r.Method, r.URL) + var boxes []Box + db.Find(&boxes) + json.NewEncoder(w).Encode(boxes) +} + +func GetBoxHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + var box Box + if err := db.First(&box, id).Error; err != nil { + http.Error(w, "Box not found", http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(box) +} + +// createBoxHandler handles the POST /boxes endpoint. +func CreateBoxHandler(w http.ResponseWriter, r *http.Request) { + var box Box + err := json.NewDecoder(r.Body).Decode(&box) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + db.Create(&box) + + // Create a response struct to include the ID + type createBoxResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + } + + response := createBoxResponse{ + ID: box.ID, + Name: box.Name, + } + + json.NewEncoder(w).Encode(response) +} + +// deleteBoxHandler handles the DELETE /boxes/{id} endpoint. +func DeleteBoxHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + // Retrieve the box from the database + var box Box + if err := db.First(&box, id).Error; err != nil { + http.Error(w, "Box not found", http.StatusNotFound) + return + } + + // Optionally, delete associated items (if you want cascading delete) + // db.Where("box_id = ?", id).Delete(&Item{}) + + // Delete the box + db.Delete(&box) + + w.WriteHeader(http.StatusNoContent) // 204 No Content +} diff --git a/handlers.go b/item_handlers.go similarity index 50% rename from handlers.go rename to item_handlers.go index 5d75070..854a06e 100644 --- a/handlers.go +++ b/item_handlers.go @@ -1,167 +1,26 @@ +// item_handlers.go package main import ( - "context" "encoding/json" "fmt" "io" "net/http" "os" - "strings" - "time" - "github.com/dgrijalva/jwt-go" "github.com/gorilla/mux" - "golang.org/x/crypto/bcrypt" ) -// Define contextKey globally within the package -type contextKey string - -// Define your key as a constant of the custom type -const userKey contextKey = "user" - -// LoginRequest represents the request body for the /login endpoint. -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// LoginResponse represents the response body for the /login endpoint. -type LoginResponse struct { - Token string `json:"token"` -} - -func init() { - fmt.Printf("handlers.go init: config = %+v", config) -} - -// loginHandler handles the /login endpoint. -// LoginHandler handles the /login endpoint. -func LoginHandler(w http.ResponseWriter, r *http.Request) { - var req LoginRequest - //fmt.Println("db is ", db) - //fmt.Println("config is ", config) - if db == nil { - fmt.Println("DB is nil") - http.Error(w, "Database not initialized", http.StatusInternalServerError) - return - } - fmt.Println("DB is not nil") - - //if config == nil { - //fmt.Println("Config is nil in LoginHandler") - //h//ttp.Error(w, "Configuration not loaded", http.StatusInternalServerError) - //return - //} - //fmt.Println("Config is not nil") - - fmt.Printf("DB: %+v\n", db) - //fmt.Printf("Config: %+v\n", config) - fmt.Println("LoginHandler called") - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - // Check if the user exists and the password matches - var user User - db.Where("username = ?", req.Username).First(&user) - if user.ID == 0 { - http.Error(w, "Invalid username or password", http.StatusUnauthorized) - return - } - - // Compare the provided password with the stored hashed password - err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)) - if err != nil { - http.Error(w, "Invalid username or password", http.StatusUnauthorized) - return - } - - // Generate JWT token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "username": user.Username, - "exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours - }) - - tokenString, err := token.SignedString(*JWTSecret) - if err != nil { - http.Error(w, "Failed to generate token", http.StatusInternalServerError) - return - } - - // Return the token in the response - json.NewEncoder(w).Encode(LoginResponse{Token: tokenString}) -} - -// getBoxesHandler handles the GET /boxes endpoint. -func GetBoxesHandler(w http.ResponseWriter, r *http.Request) { - fmt.Printf("Received %s request to %s\n", r.Method, r.URL) - var boxes []Box - db.Find(&boxes) - json.NewEncoder(w).Encode(boxes) -} - -func GetBoxHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - - var box Box - if err := db.First(&box, id).Error; err != nil { - http.Error(w, "Box not found", http.StatusNotFound) - return - } - - json.NewEncoder(w).Encode(box) -} - -// createBoxHandler handles the POST /boxes endpoint. -func CreateBoxHandler(w http.ResponseWriter, r *http.Request) { - var box Box - err := json.NewDecoder(r.Body).Decode(&box) - if err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - db.Create(&box) - - // Create a response struct to include the ID - type createBoxResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - } - - response := createBoxResponse{ - ID: box.ID, - Name: box.Name, - } - - json.NewEncoder(w).Encode(response) -} - -// deleteBoxHandler handles the DELETE /boxes/{id} endpoint. -func DeleteBoxHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - - // Retrieve the box from the database - var box Box - if err := db.First(&box, id).Error; err != nil { - http.Error(w, "Box not found", http.StatusNotFound) - return - } - - // Optionally, delete associated items (if you want cascading delete) - // db.Where("box_id = ?", id).Delete(&Item{}) - - // Delete the box - db.Delete(&box) - - w.WriteHeader(http.StatusNoContent) // 204 No Content -} +// Move all item-related handlers here: +// - GetItemsHandler +// - CreateItemHandler +// - GetItemHandler +// - UpdateItemHandler +// - DeleteItemHandler +// - UploadItemImageHandler +// - GetItemImageHandler +// - SearchItemsHandler +// - GetItemsInBoxHandler // getItemsHandler handles the GET /items endpoint. func GetItemsHandler(w http.ResponseWriter, r *http.Request) { @@ -380,55 +239,3 @@ func DeleteItemHandler(w http.ResponseWriter, r *http.Request) { db.Delete(&item) w.WriteHeader(http.StatusNoContent) } - -// authMiddleware is a middleware function that checks for a valid JWT token in the request header and enables CORS. -func AuthMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Set CORS headers - w.Header().Set("Access-Control-Allow-Origin", "*") // Replace "*" with your allowed frontend origin if needed - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - - // Handle preflight request for CORS - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - - // Get the token from the request header - tokenString := r.Header.Get("Authorization") - if tokenString == "" { - http.Error(w, "Authorization header missing", http.StatusUnauthorized) - return - } - - // Remove "Bearer " prefix from token string - tokenString = strings.Replace(tokenString, "Bearer ", "", 1) - - // Parse and validate the JWT token - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - // Make sure that the signing method is HMAC - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return *JWTSecret, nil - }) - if err != nil || !token.Valid { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - // Extract the user claims from the token - if claims, ok := token.Claims.(jwt.MapClaims); ok { - // Add the "user" claim to the request context - newCtx := context.WithValue(r.Context(), userKey, claims["username"]) - r = r.WithContext(newCtx) - } else { - http.Error(w, "Invalid token claims", http.StatusUnauthorized) - return - } - - // Call the next handler in the chain - next.ServeHTTP(w, r) - }) -}