diff --git a/api/auth.go b/api/auth.go new file mode 100644 index 0000000..ab88c00 --- /dev/null +++ b/api/auth.go @@ -0,0 +1,148 @@ +package api + +import ( + "encoding/json" + "io" + "net/http" + "os" + "sync" + + "golang.org/x/crypto/bcrypt" +) + +type Token struct { + Token string `json:"token"` +} + +type User struct { + ID int `json:"id"` + Email string `json:"email"` + Password string `json:"password"` + Token string `json:"token"` +} + +type ResponseUser struct { + ID int `json:"id"` + Email string `json:"email"` +} + +const ( + errBadRequest = "bad request" + errUnauthorized = "unauthorized" + errNotFound = "not found" + errInternalServer = "internal server error" + errMissingHeader = "missing authorization header" +) + +func decodeRequestBody(w http.ResponseWriter, r *http.Request, v interface{}) error { + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(v) + if err != nil { + http.Error(w, errBadRequest, http.StatusBadRequest) + } + return err +} + +func Login(w http.ResponseWriter, r *http.Request) { + var user User + if err := decodeRequestBody(w, r, &user); err != nil { + return + } + + // Read users from the JSON file + file, err := os.Open("users.json") + if err != nil { + http.Error(w, "Failed to open users file", http.StatusInternalServerError) + return + } + defer file.Close() + + var users []User + err = json.NewDecoder(file).Decode(&users) + if err != nil { + http.Error(w, "Failed to decode users file", http.StatusInternalServerError) + return + } + + for _, u := range users { + if u.Email == user.Email { + err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(user.Password)) + if err != nil { + http.Error(w, "Invalid password", http.StatusUnauthorized) + return + } + + json.NewEncoder(w).Encode(Token{Token: u.Token}) + return + } + } + + http.Error(w, "User not found", http.StatusNotFound) +} + +func Register(w http.ResponseWriter, r *http.Request) { + var mu sync.RWMutex + var user User + if err := decodeRequestBody(w, r, &user); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Read existing users from the file + file, err := os.Open("users.json") + if err != nil { + http.Error(w, "Failed to open users file", http.StatusInternalServerError) + return + } + + var existingUsers []User + err = json.NewDecoder(file).Decode(&existingUsers) + file.Close() + if err != nil && err != io.EOF { + http.Error(w, "Failed to read users from file", http.StatusInternalServerError) + return + } + + // Check if a user with the same email already exists + mu.Lock() + for _, u := range existingUsers { + if u.Email == user.Email { + http.Error(w, "User with this email already exists", http.StatusBadRequest) + mu.Unlock() + return + } + } + mu.Unlock() + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + http.Error(w, "Failed to hash password", http.StatusInternalServerError) + return + } + + user.ID = len(existingUsers) + 1 + user.Password = string(hashedPassword) + user.Token = string(hashedPassword) + + // Append the new user + existingUsers = append(existingUsers, user) + + // Write the users back to the file + file, err = os.OpenFile("users.json", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + http.Error(w, "Failed to open users file", http.StatusInternalServerError) + return + } + defer file.Close() + + err = json.NewEncoder(file).Encode(existingUsers) + if err != nil { + http.Error(w, "Failed to write users to file", http.StatusInternalServerError) + return + } + + // Create a new struct with only the fields you want to return + responseUser := ResponseUser{ID: user.ID, Email: user.Email} + + json.NewEncoder(w).Encode(responseUser) +} diff --git a/api/items.go b/api/items.go new file mode 100644 index 0000000..982b8b5 --- /dev/null +++ b/api/items.go @@ -0,0 +1,279 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "sync" + + "github.com/gorilla/mux" +) + +type Item struct { + ID int `json:"id"` + Name string `json:"name"` + Price float64 `json:"price"` +} + +var store = make(map[int]Item) + +func extractToken(r *http.Request) (string, error) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + return "", fmt.Errorf(errMissingHeader) + } + + // Extract the token from the Bearer string + token := strings.TrimPrefix(authHeader, "Bearer ") + return token, nil +} + +func CreateItem(w http.ResponseWriter, r *http.Request) { + var mu sync.RWMutex + token, err := extractToken(r) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + // Read existing users from the file + userfile, err := os.Open("users.json") + if err != nil { + http.Error(w, "Failed to open users file", http.StatusInternalServerError) + return + } + + var existingUsers []User + err = json.NewDecoder(userfile).Decode(&existingUsers) + userfile.Close() + if err != nil && err != io.EOF { + http.Error(w, "Failed to read users from file", http.StatusInternalServerError) + return + } + + validToken := false + for _, user := range existingUsers { + if user.Token == token { + validToken = true + break + } + } + + if !validToken { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + var item Item + if err := json.NewDecoder(r.Body).Decode(&item); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Read the existing items from the file + var items []Item + file, err := os.Open("items.json") + if err != nil { + if !os.IsNotExist(err) { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + if err := json.NewDecoder(file).Decode(&items); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + file.Close() + } + + // Find the highest ID among the existing items + highestID := 0 + for _, item := range items { + if item.ID > highestID { + highestID = item.ID + } + } + + // Assign a unique ID to the new item + mu.Lock() + item.ID = highestID + 1 + store[item.ID] = item + mu.Unlock() + + // Append the new item to the slice + items = append(items, item) + + // Write the slice back to the file + file, err = os.Create("items.json") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + if err := json.NewEncoder(file).Encode(items); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(item) +} + +func GetItems(w http.ResponseWriter, r *http.Request) { + // Open the file + file, err := os.Open("items.json") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + // Decode the items from the file + var items []Item + if err := json.NewDecoder(file).Decode(&items); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Encode the items to the response + json.NewEncoder(w).Encode(items) +} + +func GetItem(w http.ResponseWriter, r *http.Request) { + idStr := mux.Vars(r)["id"] + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, fmt.Sprintf("invalid id: %s", idStr), http.StatusBadRequest) + return + } + + // Open the file + file, err := os.Open("items.json") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + // Decode the items from the file + var items []Item + if err := json.NewDecoder(file).Decode(&items); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Find the item with the given ID + var item *Item + for _, i := range items { + if i.ID == id { + item = &i + break + } + } + + if item == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("no item with id: %d", id)}) + return + } + + json.NewEncoder(w).Encode(item) +} + +func UpdateItems(w http.ResponseWriter, r *http.Request) { + token, err := extractToken(r) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + // Read existing users from the file + userfile, err := os.Open("users.json") + if err != nil { + http.Error(w, "Failed to open users file", http.StatusInternalServerError) + return + } + + var existingUsers []User + err = json.NewDecoder(userfile).Decode(&existingUsers) + userfile.Close() + if err != nil && err != io.EOF { + http.Error(w, "Failed to read users from file", http.StatusInternalServerError) + return + } + + validToken := false + for _, user := range existingUsers { + if user.Token == token { + validToken = true + break + } + } + + if !validToken { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + // Parse the request body + var updateRequest struct { + IDs []int `json:"ids"` + Item Item `json:"item"` + } + if err := json.NewDecoder(r.Body).Decode(&updateRequest); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Open the file + file, err := os.Open("items.json") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + // Decode the items from the file + var items []Item + if err := json.NewDecoder(file).Decode(&items); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Update the items with the given IDs + updatedIDs := make(map[int]bool) + for _, id := range updateRequest.IDs { + for i, item := range items { + if item.ID == id { + if updateRequest.Item.Name != "" { + items[i].Name = updateRequest.Item.Name + } + if updateRequest.Item.Price != 0 { + items[i].Price = updateRequest.Item.Price + } + updatedIDs[id] = true + } + } + } + + // Write the items back to the file + file, err = os.Create("items.json") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + if err := json.NewEncoder(file).Encode(items); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Respond with the updated IDs + json.NewEncoder(w).Encode(updatedIDs) +} diff --git a/doc/todo.md b/doc/todo.md new file mode 100644 index 0000000..222288e --- /dev/null +++ b/doc/todo.md @@ -0,0 +1,485 @@ +To refactor the provided code and improve its organization and maintainability, you can follow these steps: + +1. **Separate concerns** : Divide the code into separate packages based on their responsibilities. For example, you can create separate packages for authentication, item management, and any other functionality you might have. +2. **Create models** : Define structs for your data models (e.g., `User`, `Item`) in separate files or packages. This will make it easier to manage and reuse these models across your application. +3. **Separate handlers** : Move the HTTP handler functions (`Login`, `Register`, `createItem`, `getItems`, `getItem`, `updateItems`) into separate files or packages based on their functionality. This will make your code more modular and easier to maintain. +4. **Introduce services** : Create separate services or repositories for handling business logic and data access. These services should be responsible for interacting with the data store (e.g., reading/writing files, interacting with a database) and performing any necessary operations on the data. +5. **Use interfaces** : Define interfaces for your services or repositories to decouple the implementation details from the rest of your application. This will make it easier to swap out the implementation or mock the services for testing purposes. +6. **Improve error handling** : Implement a consistent and robust error handling mechanism throughout your application. You can create custom error types or use existing error handling packages like `github.com/pkg/errors`. +7. **Use dependency injection** : Inject dependencies (e.g., services, repositories) into your handlers or other components that need them, rather than creating them directly. This will make your code more testable and easier to maintain. +8. **Separate configuration** : Move any configuration values (e.g., file paths, port numbers) into a separate configuration package or file. This will make it easier to manage and change these values without modifying the application code. +9. **Improve logging** : Implement a structured logging system using a logging package like `github.com/sirupsen/logrus` or `github.com/rs/zerolog`. This will make it easier to debug and monitor your application. +10. **Add tests** : Write unit tests for your services, repositories, and any other logic-containing components. This will help ensure that your code works as expected and make it easier to refactor or add new features in the future. + +Here's an example of how you could structure your code after refactoring: + +```go +myproject/ +├── cmd/ +│ └── server/ +│ └── main.go +├── config/ +│ └── config.go +├── handlers/ +│ ├── auth.go +│ └── items.go +├── models/ +│ ├── item.go +│ └── user.go +├── pkg/ +│ ├── auth/ +│ │ ├── auth.go +│ │ └── auth_test.go +│ └── items/ +│ ├── items.go +│ └── items_test.go +├── go.mod +└── go.sum +``` + +In this structure: + +- `cmd/server/main.go` is the entry point of your application, where you set up the HTTP server and routes. +- `config/config.go` contains configuration values for your application. +- `handlers/` contains the HTTP handler functions for authentication and item management. +- `models/` contains the data models for `User` and `Item`. +- `pkg/auth/` and `pkg/items/` contain the services or repositories for authentication and item management, respectively. + +By following this structure and separating concerns, your code will be more modular, easier to maintain, and easier to test. + +Sure, here's an example of how the code could be structured across different files and packages: + +**cmd/server/main.go** + +```go +package main + +import ( + "fmt" + "log" + "net/http" + "myproject/config" + "myproject/handlers" + "myproject/pkg/auth" + "myproject/pkg/items" + + "github.com/gorilla/mux" +) + +func main() { + cfg, err := config.LoadConfig() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + + authService := auth.NewService() + itemsService := items.NewService() + + r := mux.NewRouter() + + authHandler := handlers.NewAuthHandler(authService) + itemsHandler := handlers.NewItemsHandler(itemsService) + + r.HandleFunc("/register", authHandler.Register).Methods("POST") + r.HandleFunc("/login", authHandler.Login).Methods("POST") + r.HandleFunc("/items", itemsHandler.CreateItem).Methods("POST") + r.HandleFunc("/items", itemsHandler.GetItems).Methods("GET") + r.HandleFunc("/items/{id}", itemsHandler.GetItem).Methods("GET") + r.HandleFunc("/updateitems", itemsHandler.UpdateItems).Methods("PUT") + + log.Printf("Listening on port %s", cfg.Port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", cfg.Port), r)) +} +``` + +**config/config.go** + +```go +package config + +import ( + "os" +) + +type Config struct { + Port string + FilePath string +} + +func LoadConfig() (*Config, error) { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + filePath := os.Getenv("FILE_PATH") + if filePath == "" { + filePath = "data.json" + } + + return &Config{ + Port: port, + FilePath: filePath, + }, nil +} +``` + +**handlers/auth.go** + +```go +package handlers + +import ( + "encoding/json" + "net/http" + "myproject/pkg/auth" +) + +type AuthHandler struct { + authService *auth.Service +} + +func NewAuthHandler(authService *auth.Service) *AuthHandler { + return &AuthHandler{authService: authService} +} + +func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { + var user auth.User + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err := h.authService.Register(&user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(user) +} + +func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { + var user auth.User + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + token, err := h.authService.Login(&user) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(auth.Token{Token: token}) +} +``` + +**handlers/items.go** + +```go +package handlers + +import ( + "encoding/json" + "net/http" + "myproject/pkg/items" + "strconv" + + "github.com/gorilla/mux" +) + +type ItemsHandler struct { + itemsService *items.Service +} + +func NewItemsHandler(itemsService *items.Service) *ItemsHandler { + return &ItemsHandler{itemsService: itemsService} +} + +func (h *ItemsHandler) CreateItem(w http.ResponseWriter, r *http.Request) { + var item items.Item + if err := json.NewDecoder(r.Body).Decode(&item); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + token := r.Header.Get("Authorization") + if token == "" { + http.Error(w, "Missing authorization header", http.StatusUnauthorized) + return + } + + err := h.itemsService.CreateItem(&item, token) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(item) +} + +func (h *ItemsHandler) GetItems(w http.ResponseWriter, r *http.Request) { + items, err := h.itemsService.GetItems() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(items) +} + +func (h *ItemsHandler) GetItem(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid item ID", http.StatusBadRequest) + return + } + + item, err := h.itemsService.GetItem(id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if item == nil { + http.Error(w, "Item not found", http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(item) +} + +func (h *ItemsHandler) UpdateItems(w http.ResponseWriter, r *http.Request) { + var updateRequest items.UpdateRequest + if err := json.NewDecoder(r.Body).Decode(&updateRequest); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + token := r.Header.Get("Authorization") + if token == "" { + http.Error(w, "Missing authorization header", http.StatusUnauthorized) + return + } + + updatedIDs, err := h.itemsService.UpdateItems(&updateRequest, token) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(updatedIDs) +} +``` + +**models/item.go** + +```go +package models + +type Item struct { + ID int `json:"id"` + Name string `json:"name"` + Price float64 `json:"price"` +} +``` + +**models/user.go** + +```go +package models + +type User struct { + ID int `json:"id"` + Email string `json:"email"` + Password string `json:"password"` + Token string `json:"token"` +} +``` + +**pkg/auth/auth.go** + +```go +package auth + +import ( + "errors" + "myproject/models" + + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrUserNotFound = errors.New("user not found") + ErrInvalidPassword = errors.New("invalid password") +) + +type Service struct { + users []*models.User +} + +func NewService() *Service { + return &Service{ + users: make([]*models.User, 0), + } +} + +func (s *Service) Register(user *models.User) error { + // Check if user already exists + for _, u := range s.users { + if u.Email == user.Email { + return errors.New("user already exists") + } + } + + // Hash the password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + + user.Password = string(hashedPassword) + user.ID = len(s.users) + 1 + + s.users = append(s.users, user) + return nil +} + +func (s *Service) Login(user *models.User) (string, error) { + // Find the user + var foundUser *models.User + for _, u := range s.users { + if u.Email == user.Email { + foundUser = u + break + } + } + + if foundUser == nil { + return "", ErrUserNotFound + } + + // Compare the password + err := bcrypt.CompareHashAndPassword([]byte(foundUser.Password), []byte(user.Password)) + if err != nil { + return "", ErrInvalidPassword + } + + // Generate a token + token, err := generateToken() + if err != nil { + return "", err + } + + foundUser.Token = token + return token, nil +} +``` + +**pkg/items/items.go** + +```go +package items + +import ( + "errors" + "myproject/models" +) + +var ( + ErrInvalidToken = errors.New("invalid token") +) + +type Service struct { + items []*models.Item + users []*models.User +} + +func NewService() *Service { + return &Service{ + items: make([]*models.Item, 0), + users: make([]*models.User, 0), + } +} + +type UpdateRequest struct { + IDs []int `json:"ids"` + Item *models.Item `json:"item"` +} + +func (s *Service) CreateItem(item *models.Item, token string) error { + // Validate the token + if !s.isValidToken(token) { + return ErrInvalidToken + } + + // Find the highest ID + highestID := 0 + for _, i := range s.items { + if i.ID > highestID { + highestID = i.ID + } + } + + item.ID = highestID + 1 + s.items = append(s.items, item) + return nil +} + +func (s *Service) GetItems() ([]*models.Item, error) { + return s.items, nil +} + +func (s *Service) GetItem(id int) (*models.Item, error) { + for _, item := range s.items { + if item.ID == id { + return item, nil + } + } + return nil, nil +} + +func (s *Service) UpdateItems(updateRequest *UpdateRequest, token string) (map[int]bool, error) { + // Validate the token + if !s.isValidToken(token) { + return nil, ErrInvalidToken + } + + updatedIDs := make(map[int]bool) + for _, id := range updateRequest.IDs { + for i, item := range s.items { + if item.ID == id { + if updateRequest.Item.Name != "" { + s.items[i].Name = updateRequest.Item.Name + } + if updateRequest.Item.Price != 0 { + s.items[i].Price = updateRequest.Item.Price + } + updatedIDs[id] = true + } + } + } + + return updatedIDs, nil +} + +func (s *Service) isValidToken(token string) bool { + for _, user := range s.users { + if user.Token == token { + return true + } + } + return false +} +``` + +This is just an example of how you could structure your code. You might need to adjust it based on your specific requirements and add any additional functionality or error handling as needed. diff --git a/items.json b/items.json index e2a464b..653a5d4 100644 --- a/items.json +++ b/items.json @@ -1 +1 @@ -[{"id":3,"name":"item 3","price":22.5},{"id":1,"name":"New Item 1","price":1000000},{"id":2,"name":"New Item 1","price":1000000},{"id":4,"name":"item1","price":10.5},{"id":5,"name":"item1","price":10.5},{"id":6,"name":"item1","price":10.5},{"id":7,"name":"item-name","price":10.5}] +[{"id":3,"name":"item 3","price":22.5},{"id":1,"name":"New Item 1","price":1000000},{"id":2,"name":"New Item 1","price":1000000},{"id":4,"name":"item1","price":10.5},{"id":5,"name":"item1","price":10.5},{"id":6,"name":"item1","price":10.5},{"id":7,"name":"item-name","price":10.5},{"id":8,"name":"item-name","price":100000.5},{"id":9,"name":"item-name","price":100000.5}] diff --git a/main.go b/main.go index 69662ae..a838bfc 100644 --- a/main.go +++ b/main.go @@ -1,59 +1,24 @@ package main import ( - "encoding/json" "fmt" "log" "net/http" "os" - "strconv" - "strings" - "sync" + "github.com/andrewnovykov/simple-api-on-go/api" "github.com/gorilla/mux" - "golang.org/x/crypto/bcrypt" -) - -type Item struct { - ID int `json:"id"` - Name string `json:"name"` - Price float64 `json:"price"` -} - -type User struct { - ID int `json:"id"` - Email string `json:"email"` - Password string `json:"password"` - Token string `json:"token,omitempty"` -} - -type Token struct { - Token string `json:"token"` -} - -var ( - store = make(map[int]Item) - users = make(map[int]User) - mu sync.RWMutex -) - -const ( - errBadRequest = "bad request" - errUnauthorized = "unauthorized" - errNotFound = "not found" - errInternalServer = "internal server error" - errMissingHeader = "missing authorization header" ) func main() { r := mux.NewRouter() - r.HandleFunc("/items", createItem).Methods("POST") - r.HandleFunc("/items", getItems).Methods("GET") - r.HandleFunc("/items/{id}", getItem).Methods("GET") - r.HandleFunc("/updateitems", updateItems).Methods("PUT") - r.HandleFunc("/register", register).Methods("POST") - r.HandleFunc("/login", login).Methods("POST") + r.HandleFunc("/items", api.CreateItem).Methods("POST") + r.HandleFunc("/items", api.GetItems).Methods("GET") + r.HandleFunc("/items/{id}", api.GetItem).Methods("GET") + r.HandleFunc("/updateitems", api.UpdateItems).Methods("PUT") + r.HandleFunc("/register", api.Register).Methods("POST") + r.HandleFunc("/login", api.Login).Methods("POST") port := os.Getenv("PORT") @@ -64,323 +29,3 @@ func main() { log.Println("Listening on port", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), r)) } - -func extractToken(r *http.Request) (string, error) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - return "", fmt.Errorf(errMissingHeader) - } - - // Extract the token from the Bearer string - token := strings.TrimPrefix(authHeader, "Bearer ") - return token, nil -} - -func createItem(w http.ResponseWriter, r *http.Request) { - token, err := extractToken(r) - if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - validToken := false - for _, user := range users { - if user.Token == token { - validToken = true - break - } - } - - if !validToken { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - var item Item - if err := json.NewDecoder(r.Body).Decode(&item); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Read the existing items from the file - var items []Item - file, err := os.Open("items.json") - if err != nil { - if !os.IsNotExist(err) { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } else { - if err := json.NewDecoder(file).Decode(&items); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - file.Close() - } - - // Find the highest ID among the existing items - highestID := 0 - for _, item := range items { - if item.ID > highestID { - highestID = item.ID - } - } - - // Assign a unique ID to the new item - mu.Lock() - item.ID = highestID + 1 - store[item.ID] = item - mu.Unlock() - - // Append the new item to the slice - items = append(items, item) - - // Write the slice back to the file - file, err = os.Create("items.json") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - if err := json.NewEncoder(file).Encode(items); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(item) -} - -func getItems(w http.ResponseWriter, r *http.Request) { - // Open the file - file, err := os.Open("items.json") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - // Decode the items from the file - var items []Item - if err := json.NewDecoder(file).Decode(&items); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Encode the items to the response - json.NewEncoder(w).Encode(items) -} - -func getItem(w http.ResponseWriter, r *http.Request) { - idStr := mux.Vars(r)["id"] - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, fmt.Sprintf("invalid id: %s", idStr), http.StatusBadRequest) - return - } - - // Open the file - file, err := os.Open("items.json") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - // Decode the items from the file - var items []Item - if err := json.NewDecoder(file).Decode(&items); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Find the item with the given ID - var item *Item - for _, i := range items { - if i.ID == id { - item = &i - break - } - } - - if item == nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("no item with id: %d", id)}) - return - } - - json.NewEncoder(w).Encode(item) -} - -func updateItems(w http.ResponseWriter, r *http.Request) { - token, err := extractToken(r) - if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - validToken := false - for _, user := range users { - if user.Token == token { - validToken = true - break - } - } - - if !validToken { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - // Parse the request body - var updateRequest struct { - IDs []int `json:"ids"` - Item Item `json:"item"` - } - if err := json.NewDecoder(r.Body).Decode(&updateRequest); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Open the file - file, err := os.Open("items.json") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - // Decode the items from the file - var items []Item - if err := json.NewDecoder(file).Decode(&items); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Update the items with the given IDs - updatedIDs := make(map[int]bool) - for _, id := range updateRequest.IDs { - for i, item := range items { - if item.ID == id { - if updateRequest.Item.Name != "" { - items[i].Name = updateRequest.Item.Name - } - if updateRequest.Item.Price != 0 { - items[i].Price = updateRequest.Item.Price - } - updatedIDs[id] = true - } - } - } - - // Write the items back to the file - file, err = os.Create("items.json") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - if err := json.NewEncoder(file).Encode(items); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Respond with the updated IDs - json.NewEncoder(w).Encode(updatedIDs) -} - -func decodeRequestBody(w http.ResponseWriter, r *http.Request, v interface{}) error { - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(v) - if err != nil { - http.Error(w, errBadRequest, http.StatusBadRequest) - } - return err -} - -func login(w http.ResponseWriter, r *http.Request) { - var user User - if err := decodeRequestBody(w, r, &user); err != nil { - return - } - - // Read users from the JSON file - file, err := os.Open("users.json") - if err != nil { - http.Error(w, "Failed to open users file", http.StatusInternalServerError) - return - } - defer file.Close() - - users := make(map[int]User) - err = json.NewDecoder(file).Decode(&users) - if err != nil { - http.Error(w, "Failed to decode users file", http.StatusInternalServerError) - return - } - - for _, u := range users { - if u.Email == user.Email { - err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(user.Password)) - if err != nil { - http.Error(w, "Invalid password", http.StatusUnauthorized) - return - } - - json.NewEncoder(w).Encode(Token{Token: u.Token}) - return - } - } - - http.Error(w, "User not found", http.StatusNotFound) -} - -func register(w http.ResponseWriter, r *http.Request) { - var user User - if err := decodeRequestBody(w, r, &user); err != nil { - return - } - - // Check if a user with the same email already exists - mu.Lock() - for _, u := range users { - if u.Email == user.Email { - http.Error(w, "User with this email already exists", http.StatusBadRequest) - mu.Unlock() - return - } - } - mu.Unlock() - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) - if err != nil { - http.Error(w, "Failed to hash password", http.StatusInternalServerError) - return - } - - user.ID = len(users) + 1 - user.Password = string(hashedPassword) - user.Token = fmt.Sprintf("token%d", user.ID) - - mu.Lock() - users[user.ID] = user - mu.Unlock() - - // Save users to a JSON file - file, err := os.OpenFile("users.json", os.O_RDWR|os.O_CREATE, 0755) - if err != nil { - http.Error(w, "Failed to open users file", http.StatusInternalServerError) - return - } - defer file.Close() - - json.NewEncoder(file).Encode(users) - - // Create a new struct with only the fields you want to return - responseUser := struct { - ID int `json:"id"` - Email string `json:"email"` - }{ID: user.ID, Email: user.Email} - - json.NewEncoder(w).Encode(responseUser) -} diff --git a/users.json b/users.json index 6cbbddd..c322fa0 100755 --- a/users.json +++ b/users.json @@ -1 +1,38 @@ -{"1":{"id":1,"email":"your-email@example.com","password":"$2a$10$HtNZ56EyG2yEn50iCEmwbOSRIDXlxBGviXSP/NTNhmUXv5IiooGbS","token":"token1"},"2":{"id":2,"email":"your-email@example1.com","password":"$2a$10$2RNClQ9WRpjzLISHowOPr.Xsus3bX8q7IPs1vqLyHdXk/2stMLti2","token":"token2"}} +[ + { + "id": 1, + "email": "your-email-1@example.com", + "password": "$2a$10$H98DkpBqt2jkvpugce01veTu9suo9QDQgkVr24mx9WLTS40ZDsEku", + "token": "$2a$10$H98DkpBqt2jkvpugce01veTu9suo9QDQgkVr24mx9WLTS40ZDsEku" + }, + { + "id": 2, + "email": "your-email-2@example.com", + "password": "$2a$10$DkBcr8Xx7njWmc0b.vWGge7IPAawTuxRsT.Ox8TRB2MQERJ4bc8Ue", + "token": "$2a$10$DkBcr8Xx7njWmc0b.vWGge7IPAawTuxRsT.Ox8TRB2MQERJ4bc8Ue" + }, + { + "id": 3, + "email": "your-email-3@example.com", + "password": "$2a$10$/0T.MTLEBVKz0AooI86acejssOhQV.1puQX82B9MMiEyLVF1J6.FO", + "token": "$2a$10$/0T.MTLEBVKz0AooI86acejssOhQV.1puQX82B9MMiEyLVF1J6.FO" + }, + { + "id": 4, + "email": "your-email-4@example.com", + "password": "$2a$10$.7TQKCLvryN5JyvI01JLXeTp1f39wsTNRcIbYphVL1ttKqKUFiw5C", + "token": "$2a$10$.7TQKCLvryN5JyvI01JLXeTp1f39wsTNRcIbYphVL1ttKqKUFiw5C" + }, + { + "id": 5, + "email": "your-email-5@example.com", + "password": "$2a$10$OovxXusBHigLS/wJbAYuHuC0FCYgrIBGZUJdjggxsVtkSlMTiG5iS", + "token": "$2a$10$OovxXusBHigLS/wJbAYuHuC0FCYgrIBGZUJdjggxsVtkSlMTiG5iS" + }, + { + "id": 6, + "email": "your-email-6@example.com", + "password": "$2a$10$LdQcB.TAskK42aBX386JPuPkATln.ROxTPHR2B4ksv/VQC13.9mf.", + "token": "$2a$10$LdQcB.TAskK42aBX386JPuPkATln.ROxTPHR2B4ksv/VQC13.9mf." + } +] \ No newline at end of file