Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Steve White | 63dce0b348 |
|
@ -2,5 +2,3 @@
|
||||||
data/*
|
data/*
|
||||||
images/*
|
images/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
build/*
|
|
||||||
boxes-api
|
|
||||||
|
|
16
README.md
16
README.md
|
@ -4,7 +4,6 @@
|
||||||
This project is a backend API built using Go, designed for managing boxes and the items stored in those boxes. The application is containerized using Docker and uses SQLite3 for data storage. It also supports JWT-based authentication and logs various activities such as logins and box/item operations.
|
This project is a backend API built using Go, designed for managing boxes and the items stored in those boxes. The application is containerized using Docker and uses SQLite3 for data storage. It also supports JWT-based authentication and logs various activities such as logins and box/item operations.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Manage boxes and items stored within them.
|
- Manage boxes and items stored within them.
|
||||||
- JWT-based authentication.
|
- JWT-based authentication.
|
||||||
- Automatic database creation if it doesn't exist.
|
- Automatic database creation if it doesn't exist.
|
||||||
|
@ -14,7 +13,6 @@ This project is a backend API built using Go, designed for managing boxes and th
|
||||||
- Containerized using Docker.
|
- Containerized using Docker.
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Language:** Go
|
- **Language:** Go
|
||||||
- **Database:** SQLite3
|
- **Database:** SQLite3
|
||||||
- **Authentication:** JWT
|
- **Authentication:** JWT
|
||||||
|
@ -23,12 +21,10 @@ This project is a backend API built using Go, designed for managing boxes and th
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
- **POST** `/login`
|
- **POST** `/login`
|
||||||
Authenticates a user and returns a JWT.
|
Authenticates a user and returns a JWT.
|
||||||
|
|
||||||
### Boxes
|
### Boxes
|
||||||
|
|
||||||
- **GET** `/boxes`
|
- **GET** `/boxes`
|
||||||
Retrieves all boxes.
|
Retrieves all boxes.
|
||||||
- **POST** `/boxes`
|
- **POST** `/boxes`
|
||||||
|
@ -39,7 +35,6 @@ This project is a backend API built using Go, designed for managing boxes and th
|
||||||
Retrieves all items in a box by its ID.
|
Retrieves all items in a box by its ID.
|
||||||
|
|
||||||
### Items
|
### Items
|
||||||
|
|
||||||
- **GET** `/items`
|
- **GET** `/items`
|
||||||
Retrieves all items, optionally searchable by description.
|
Retrieves all items, optionally searchable by description.
|
||||||
- **POST** `/items`
|
- **POST** `/items`
|
||||||
|
@ -65,11 +60,11 @@ jwt_secret: "your_jwt_secret"
|
||||||
log_file: "./logs/app.log"
|
log_file: "./logs/app.log"
|
||||||
image_storage_directory: "./images"
|
image_storage_directory: "./images"
|
||||||
```
|
```
|
||||||
|
The server also expects the CONFIG environment variable to be set. e.g. `export CONFIG=config/config.yaml`.
|
||||||
|
|
||||||
## Setup and Running
|
## Setup and Running
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Docker
|
- Docker
|
||||||
- Go (if running locally)
|
- Go (if running locally)
|
||||||
- SQLite3
|
- SQLite3
|
||||||
|
@ -77,13 +72,11 @@ image_storage_directory: "./images"
|
||||||
### Running with Docker
|
### Running with Docker
|
||||||
|
|
||||||
1. Build the Docker image:
|
1. Build the Docker image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t box-management-api .
|
docker build -t box-management-api .
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run the Docker container:
|
2. Run the Docker container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 8080:8080 box-management-api
|
docker run -p 8080:8080 box-management-api
|
||||||
```
|
```
|
||||||
|
@ -91,20 +84,17 @@ image_storage_directory: "./images"
|
||||||
### Running Locally
|
### Running Locally
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/your-repo/box-management-api.git
|
git clone https://github.com/your-repo/box-management-api.git
|
||||||
cd box-management-api
|
cd box-management-api
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Build the Go application:
|
2. Build the Go application:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -o main .
|
go build -o main .
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Run the application:
|
3. Run the application:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./main
|
./main
|
||||||
```
|
```
|
||||||
|
@ -117,7 +107,7 @@ The application creates the following SQLite3 tables:
|
||||||
- `items`: Contains `id` (int), `name` (text), `description` (text), `box_id` (int), and `image_path` (text).
|
- `items`: Contains `id` (int), `name` (text), `description` (text), `box_id` (int), and `image_path` (text).
|
||||||
- `users`: Contains `id` (int), `username` (text), and `password` (hashed).
|
- `users`: Contains `id` (int), `username` (text), and `password` (hashed).
|
||||||
|
|
||||||
## Authentication API
|
## Authentication
|
||||||
|
|
||||||
The API uses JWT-based authentication. A default user is created on startup:
|
The API uses JWT-based authentication. A default user is created on startup:
|
||||||
|
|
||||||
|
@ -127,11 +117,9 @@ The API uses JWT-based authentication. A default user is created on startup:
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
The following events are logged to the file specified in the configuration:
|
The following events are logged to the file specified in the configuration:
|
||||||
|
|
||||||
- User logins
|
- User logins
|
||||||
- Box creation/deletion
|
- Box creation/deletion
|
||||||
- Item creation/deletion
|
- Item creation/deletion
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
This project is licensed under the MIT License.
|
||||||
|
|
294
admin.go
294
admin.go
|
@ -1,294 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetUsersHandler handles GET requests to /admin/user
|
|
||||||
func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
username := r.Context().Value(userKey).(string)
|
|
||||||
|
|
||||||
log.Info("Admin request to get all users by user: %s", username)
|
|
||||||
|
|
||||||
var users []User
|
|
||||||
if err := db.Find(&users).Error; err != nil {
|
|
||||||
log.Error("Failed to fetch users: %v", err)
|
|
||||||
http.Error(w, "Failed to fetch users", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Successfully retrieved %d users", len(users))
|
|
||||||
json.NewEncoder(w).Encode(users)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateUserHandler handles POST requests to /admin/user
|
|
||||||
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
adminUser := r.Context().Value(userKey).(string)
|
|
||||||
|
|
||||||
var user User
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&user)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to decode user creation request: %v", err)
|
|
||||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Admin user %s attempting to create new user: %s", adminUser, user.Username)
|
|
||||||
|
|
||||||
// Hash the password before storing
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to hash password for new user %s: %v", user.Username, err)
|
|
||||||
http.Error(w, "Failed to hash password", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user.Password = string(hashedPassword)
|
|
||||||
|
|
||||||
if err := db.Create(&user).Error; err != nil {
|
|
||||||
log.Error("Failed to create user %s: %v", user.Username, err)
|
|
||||||
http.Error(w, "Failed to create user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("create", fmt.Sprintf("User %s created by admin %s", user.Username, adminUser))
|
|
||||||
json.NewEncoder(w).Encode(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserHandler handles GET requests to /admin/user/{id}
|
|
||||||
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
adminUser := r.Context().Value(userKey).(string)
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
log.Info("Admin user %s requesting details for user ID: %s", adminUser, id)
|
|
||||||
|
|
||||||
var user User
|
|
||||||
if err := db.First(&user, id).Error; err != nil {
|
|
||||||
log.Warn("User not found with ID %s, requested by admin %s", id, adminUser)
|
|
||||||
http.Error(w, "User not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Successfully retrieved user %s details", user.Username)
|
|
||||||
json.NewEncoder(w).Encode(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteUserHandler handles DELETE requests to /admin/user/{id}
|
|
||||||
func DeleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
adminUser := r.Context().Value(userKey).(string)
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
log.Info("Admin user %s attempting to delete user ID: %s", adminUser, id)
|
|
||||||
|
|
||||||
var user User
|
|
||||||
if err := db.First(&user, id).Error; err != nil {
|
|
||||||
log.Warn("Attempt to delete non-existent user ID %s by admin %s", id, adminUser)
|
|
||||||
http.Error(w, "User not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Delete(&user).Error; err != nil {
|
|
||||||
log.Error("Failed to delete user %s: %v", user.Username, err)
|
|
||||||
http.Error(w, "Failed to delete user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("delete", fmt.Sprintf("User %s deleted by admin %s", user.Username, adminUser))
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupDatabaseHandler handles GET requests to /admin/db
|
|
||||||
func BackupDatabaseHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
adminUser := r.Context().Value(userKey).(string)
|
|
||||||
|
|
||||||
log.Info("Database backup requested by admin user: %s", adminUser)
|
|
||||||
|
|
||||||
// Open the database file using the path from the config
|
|
||||||
file, err := os.Open(*DatabasePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to open database file for backup: %v", err)
|
|
||||||
http.Error(w, "Failed to open database file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// Copy the file to the response writer
|
|
||||||
_, err = io.Copy(w, file)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to send database backup: %v", err)
|
|
||||||
http.Error(w, "Failed to send database file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("backup", fmt.Sprintf("Database backup created by admin %s", adminUser))
|
|
||||||
log.Info("Database backup successfully completed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// RestoreDatabaseHandler handles POST requests to /admin/db
|
|
||||||
func RestoreDatabaseHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
adminUser := r.Context().Value(userKey).(string)
|
|
||||||
|
|
||||||
log.Info("Database restore requested by admin user: %s", adminUser)
|
|
||||||
|
|
||||||
// Create a backup of the existing database
|
|
||||||
err := createDatabaseBackup()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to create backup before restore: %v", err)
|
|
||||||
http.Error(w, "Failed to create database backup", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("Created backup of existing database")
|
|
||||||
|
|
||||||
// Save the new database
|
|
||||||
err = saveNewDatabase(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to save new database during restore: %v", err)
|
|
||||||
http.Error(w, "Failed to save new database", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("Saved new database file")
|
|
||||||
|
|
||||||
// Validate the new database is properly initialized
|
|
||||||
err = validateNewDatabase()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("New database validation failed: %v", err)
|
|
||||||
http.Error(w, "New database is not properly initialized", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("New database validation successful")
|
|
||||||
|
|
||||||
// Switch to the new database app-wide
|
|
||||||
err = switchToNewDatabase()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to switch to new database: %v", err)
|
|
||||||
http.Error(w, "Failed to switch to new database", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("restore", fmt.Sprintf("Database restored by admin %s", adminUser))
|
|
||||||
log.Info("Database restore completed successfully")
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"message": "Database restored successfully"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDatabaseBackup() error {
|
|
||||||
log := GetLogger()
|
|
||||||
backupPath := *DatabasePath + ".bak"
|
|
||||||
|
|
||||||
log.Info("Creating database backup at: %s", backupPath)
|
|
||||||
|
|
||||||
src, err := os.Open(*DatabasePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to open source database for backup: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer src.Close()
|
|
||||||
|
|
||||||
dst, err := os.Create(backupPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to create backup file: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dst.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(dst, src)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to copy database to backup: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Database backup created successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveNewDatabase(r *http.Request) error {
|
|
||||||
log := GetLogger()
|
|
||||||
|
|
||||||
file, _, err := r.FormFile("database")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to get database file from request: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
dst, err := os.Create(*DatabasePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to create new database file: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dst.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(dst, file)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to save new database file: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("New database file saved successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateNewDatabase() error {
|
|
||||||
log := GetLogger()
|
|
||||||
|
|
||||||
log.Info("Validating new database")
|
|
||||||
|
|
||||||
db, err := ConnectDB(*DatabasePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to connect to new database: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Check if required tables exist
|
|
||||||
tables := []string{"users", "boxes", "items"}
|
|
||||||
for _, table := range tables {
|
|
||||||
var count int
|
|
||||||
err := db.Debug().Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?;", table).Row().Scan(&count)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error checking for table %s: %v", table, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
log.Error("Required table %s does not exist in new database", table)
|
|
||||||
return fmt.Errorf("table %s does not exist", table)
|
|
||||||
}
|
|
||||||
log.Info("Validated table exists: %s", table)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Database validation completed successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func switchToNewDatabase() error {
|
|
||||||
log := GetLogger()
|
|
||||||
|
|
||||||
log.Info("Switching to new database")
|
|
||||||
|
|
||||||
newDB, err := ConnectDB(*DatabasePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to connect to new database during switch: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the global db variable
|
|
||||||
db = newDB
|
|
||||||
|
|
||||||
log.Info("Successfully switched to new database")
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,490 +1,128 @@
|
||||||
# Boxes API Specification
|
# Boxes API - Frontend Specification
|
||||||
|
|
||||||
## Base URL
|
This document outlines the API endpoints for a simple inventory management system called "Boxes".
|
||||||
`/api/v1`
|
|
||||||
|
|
||||||
## Authentication
|
**Authentication:**
|
||||||
- All endpoints except `/login` require JWT authentication
|
|
||||||
- JWT token must be provided in the Authorization header as `Bearer <token>`
|
|
||||||
- Tokens expire after 24 hours
|
|
||||||
|
|
||||||
## Endpoints
|
* All endpoints (except `/login`) require a valid JWT token in the `Authorization` header, formatted as `Bearer <token>`.
|
||||||
|
* To obtain a token, send a POST request to `/login` with the following JSON payload:
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
#### Login
|
|
||||||
- **URL**: `/login`
|
|
||||||
- **Method**: `POST`
|
|
||||||
- **Auth Required**: No
|
|
||||||
- **Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "string",
|
|
||||||
"password": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "string"
|
"username": "your_username",
|
||||||
|
"password": "your_password"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- **Error Responses**:
|
|
||||||
- **Code**: 401 UNAUTHORIZED
|
|
||||||
- **Content**: "Invalid username or password"
|
|
||||||
|
|
||||||
### Boxes
|
* Successful login will return a JSON response with the token:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "your_jwt_token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Endpoints:**
|
||||||
|
|
||||||
|
**1. Boxes:**
|
||||||
|
|
||||||
|
* **GET /boxes:**
|
||||||
|
* Returns a list of all boxes.
|
||||||
|
* Response: Array of Box objects
|
||||||
|
|
||||||
#### Get All Boxes
|
|
||||||
- **URL**: `/boxes`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ID": "number",
|
"id": 1,
|
||||||
"CreatedAt": "timestamp",
|
"name": "Kitchen"
|
||||||
"UpdatedAt": "timestamp",
|
},
|
||||||
"DeletedAt": "timestamp|null",
|
{
|
||||||
"name": "string"
|
"id": 2,
|
||||||
|
"name": "Bedroom"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Create Box
|
* **POST /boxes:**
|
||||||
- **URL**: `/boxes`
|
* Creates a new box.
|
||||||
- **Method**: `POST`
|
* Request body: JSON object with the box name
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "number",
|
"name": "New Box"
|
||||||
"name": "string"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Get Box
|
* Response: JSON object with the created box's ID and name
|
||||||
- **URL**: `/boxes/{id}`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ID": "number",
|
"id": 3,
|
||||||
"CreatedAt": "timestamp",
|
"name": "New Box"
|
||||||
"UpdatedAt": "timestamp",
|
|
||||||
"DeletedAt": "timestamp|null",
|
|
||||||
"name": "string"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Box not found"
|
|
||||||
|
|
||||||
#### Delete Box
|
* **DELETE /boxes/{id}:**
|
||||||
- **URL**: `/boxes/{id}`
|
* Deletes the box with the specified ID.
|
||||||
- **Method**: `DELETE`
|
* Response: 204 No Content
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 204 NO CONTENT
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Box not found"
|
|
||||||
|
|
||||||
### Items
|
**2. Items:**
|
||||||
|
|
||||||
|
* **GET /items:**
|
||||||
|
* Returns a list of all items.
|
||||||
|
* Response: Array of Item objects
|
||||||
|
|
||||||
#### Get All Items
|
|
||||||
- **URL**: `/items`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ID": "number",
|
"id": 1,
|
||||||
"CreatedAt": "timestamp",
|
"name": "Fork",
|
||||||
"UpdatedAt": "timestamp",
|
"description": "Silverware",
|
||||||
"DeletedAt": "timestamp|null",
|
"box_id": 1,
|
||||||
"name": "string",
|
"image_path": "path/to/image.jpg"
|
||||||
"description": "string",
|
},
|
||||||
"box_id": "number",
|
{
|
||||||
"image_path": "string",
|
"id": 2,
|
||||||
"tags": [
|
"name": "Pillow",
|
||||||
{
|
"description": "Fluffy",
|
||||||
"ID": "number",
|
"box_id": 2,
|
||||||
"name": "string",
|
"image_path": "path/to/another_image.png"
|
||||||
"description": "string",
|
|
||||||
"color": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Create Item
|
* **POST /items:**
|
||||||
- **URL**: `/items`
|
* Creates a new item.
|
||||||
- **Method**: `POST`
|
* Request body: JSON object with item details
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"box_id": "number"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "number",
|
"name": "Spoon",
|
||||||
"name": "string"
|
"description": "For soup",
|
||||||
|
"box_id": 1,
|
||||||
|
"image_path": "path/to/spoon_image.jpg"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Get Item
|
* Response: JSON object with the created item's ID and name
|
||||||
- **URL**: `/items/{id}`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ID": "number",
|
"id": 3,
|
||||||
"CreatedAt": "timestamp",
|
"name": "Spoon"
|
||||||
"UpdatedAt": "timestamp",
|
|
||||||
"DeletedAt": "timestamp|null",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"box_id": "number",
|
|
||||||
"image_path": "string",
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"ID": "number",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"color": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Item not found"
|
|
||||||
|
|
||||||
#### Update Item
|
|
||||||
- **URL**: `/items/{id}`
|
|
||||||
- **Method**: `PUT`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"box_id": "number"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Updated item object
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Item not found"
|
|
||||||
|
|
||||||
#### Delete Item
|
|
||||||
- **URL**: `/items/{id}`
|
|
||||||
- **Method**: `DELETE`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 204 NO CONTENT
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Item not found"
|
|
||||||
|
|
||||||
#### Get Items in Box
|
|
||||||
- **URL**: `/boxes/{id}/items`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Array of item objects
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Items not found"
|
|
||||||
|
|
||||||
#### Search Items
|
|
||||||
- **URL**: `/search/items`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Query Parameters**:
|
|
||||||
- `q`: Search query string
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Array of matching item objects
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 400 BAD REQUEST
|
|
||||||
- **Content**: "Search query is required"
|
|
||||||
|
|
||||||
### Item Images
|
|
||||||
|
|
||||||
#### Upload Item Image
|
|
||||||
- **URL**: `/items/{id}/upload`
|
|
||||||
- **Method**: `POST`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Content-Type**: `multipart/form-data`
|
|
||||||
- **Form Parameters**:
|
|
||||||
- `image`: File upload
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"imagePath": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Error Responses**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Item not found"
|
|
||||||
- **Code**: 400 BAD REQUEST
|
|
||||||
- **Content**: "Unable to parse form"
|
|
||||||
|
|
||||||
#### Get Item Image
|
|
||||||
- **URL**: `/items/{id}/image`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Image file
|
|
||||||
- **Note**: Returns default image if no image is found
|
|
||||||
|
|
||||||
### Tags
|
|
||||||
|
|
||||||
#### Get All Tags
|
|
||||||
- **URL**: `/tags`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"ID": "number",
|
|
||||||
"CreatedAt": "timestamp",
|
|
||||||
"UpdatedAt": "timestamp",
|
|
||||||
"DeletedAt": "timestamp|null",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"color": "string" // hex color code
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Create Tag
|
|
||||||
- **URL**: `/tags`
|
|
||||||
- **Method**: `POST`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"color": "string" // hex color code, optional
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Created tag object
|
|
||||||
- **Error Responses**:
|
|
||||||
- **Code**: 400 BAD REQUEST
|
|
||||||
- **Content**: "Tag name is required"
|
|
||||||
- **Code**: 409 CONFLICT
|
|
||||||
- **Content**: "Tag name already exists"
|
|
||||||
|
|
||||||
#### Update Tag
|
|
||||||
- **URL**: `/tags/{id}`
|
|
||||||
- **Method**: `PUT`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"color": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Updated tag object
|
|
||||||
- **Error Responses**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Tag not found"
|
|
||||||
- **Code**: 409 CONFLICT
|
|
||||||
- **Content**: "Tag name already exists"
|
|
||||||
|
|
||||||
#### Delete Tag
|
|
||||||
- **URL**: `/tags/{id}`
|
|
||||||
- **Method**: `DELETE`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 204 NO CONTENT
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Tag not found"
|
|
||||||
|
|
||||||
### Item Tags
|
|
||||||
|
|
||||||
#### Add Tags to Item
|
|
||||||
- **URL**: `/items/{id}/tags`
|
|
||||||
- **Method**: `POST`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Request Body**:
|
|
||||||
```json
|
|
||||||
[1, 2, 3] // Array of tag IDs
|
|
||||||
```
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Updated item object with tags
|
|
||||||
- **Error Responses**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Item not found"
|
|
||||||
- **Code**: 400 BAD REQUEST
|
|
||||||
- **Content**: "Tag {id} not found"
|
|
||||||
|
|
||||||
#### Remove Tag from Item
|
|
||||||
- **URL**: `/items/{id}/tags/{tagId}`
|
|
||||||
- **Method**: `DELETE`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 204 NO CONTENT
|
|
||||||
- **Error Responses**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Item not found" or "Tag not found"
|
|
||||||
|
|
||||||
#### Get Items by Tag
|
|
||||||
- **URL**: `/tags/{id}/items`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"ID": "number",
|
|
||||||
"CreatedAt": "timestamp",
|
|
||||||
"UpdatedAt": "timestamp",
|
|
||||||
"DeletedAt": "timestamp|null",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"box_id": "number",
|
|
||||||
"image_path": "string",
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"ID": "number",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"color": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "Tag not found"
|
|
||||||
|
|
||||||
### Admin Endpoints
|
|
||||||
|
|
||||||
#### Get All Users
|
|
||||||
- **URL**: `/admin/user`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Array of user objects
|
|
||||||
|
|
||||||
#### Create User
|
|
||||||
- **URL**: `/admin/user`
|
|
||||||
- **Method**: `POST`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "string",
|
|
||||||
"password": "string",
|
|
||||||
"email": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Created user object
|
|
||||||
|
|
||||||
#### Get User
|
|
||||||
- **URL**: `/admin/user/{id}`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: User object
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "User not found"
|
|
||||||
|
|
||||||
#### Delete User
|
|
||||||
- **URL**: `/admin/user/{id}`
|
|
||||||
- **Method**: `DELETE`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 204 NO CONTENT
|
|
||||||
- **Error Response**:
|
|
||||||
- **Code**: 404 NOT FOUND
|
|
||||||
- **Content**: "User not found"
|
|
||||||
|
|
||||||
#### Backup Database
|
|
||||||
- **URL**: `/admin/db`
|
|
||||||
- **Method**: `GET`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**: Database file
|
|
||||||
|
|
||||||
#### Restore Database
|
|
||||||
- **URL**: `/admin/db`
|
|
||||||
- **Method**: `POST`
|
|
||||||
- **Auth Required**: Yes
|
|
||||||
- **Content-Type**: `multipart/form-data`
|
|
||||||
- **Form Parameters**:
|
|
||||||
- `database`: Database file
|
|
||||||
- **Success Response**:
|
|
||||||
- **Code**: 200
|
|
||||||
- **Content**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Database restored successfully"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Error Responses
|
* **GET /items/{id}:**
|
||||||
All endpoints may return these common errors:
|
* Retrieves the item with the specified ID.
|
||||||
- **401 Unauthorized**: Missing or invalid authentication token
|
* Response: Item object
|
||||||
- **500 Internal Server Error**: Server-side error
|
* **GET /boxes/{id}/items:**
|
||||||
|
* Retrieves all items within the box with the specified ID.
|
||||||
|
* Response: Array of Item objects
|
||||||
|
* **PUT /items/{id}:**
|
||||||
|
* Updates the item with the specified ID.
|
||||||
|
* Request body: JSON object with updated item details
|
||||||
|
* Response: Updated Item object
|
||||||
|
* **DELETE /items/{id}:**
|
||||||
|
* Deletes the item with the specified ID.
|
||||||
|
* Response: 204 No Content
|
||||||
|
|
191
auth.go
191
auth.go
|
@ -1,191 +0,0 @@
|
||||||
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() {
|
|
||||||
log := GetLogger()
|
|
||||||
if log != nil {
|
|
||||||
log.Info("Initializing authentication module")
|
|
||||||
log.Debug("Current config: %+v", config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginHandler handles the /login endpoint.
|
|
||||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
var req LoginRequest
|
|
||||||
|
|
||||||
if db == nil {
|
|
||||||
if log != nil {
|
|
||||||
log.Error("Database connection not initialized in LoginHandler")
|
|
||||||
}
|
|
||||||
http.Error(w, "Database not initialized", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if log != nil {
|
|
||||||
log.Info("Processing login request")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err != nil {
|
|
||||||
if log != nil {
|
|
||||||
log.Error("Failed to decode login request: %v", err)
|
|
||||||
}
|
|
||||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user exists and the password matches
|
|
||||||
var user User
|
|
||||||
result := db.Where("username = ?", req.Username).First(&user)
|
|
||||||
if result.Error != nil || user.ID == 0 {
|
|
||||||
if log != nil {
|
|
||||||
log.Warn("Login attempt failed for username: %s - user not found", req.Username)
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
if log != nil {
|
|
||||||
log.Warn("Login attempt failed for username: %s - invalid password", req.Username)
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
if log != nil {
|
|
||||||
log.Error("Failed to generate JWT token for user %s: %v", user.Username, err)
|
|
||||||
}
|
|
||||||
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if log != nil {
|
|
||||||
log.Info("Successful login for user: %s", user.Username)
|
|
||||||
log.UserAction(user.Username, "login")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
log := GetLogger()
|
|
||||||
|
|
||||||
// Set CORS headers
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a browser requesting HTML
|
|
||||||
acceptHeader := r.Header.Get("Accept")
|
|
||||||
isBrowserRequest := strings.Contains(acceptHeader, "text/html")
|
|
||||||
|
|
||||||
// If it's a browser request for HTML, always serve the React app
|
|
||||||
if isBrowserRequest {
|
|
||||||
http.Redirect(w, r, "/index.html", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// From here on, we're dealing with actual API requests
|
|
||||||
tokenString := r.Header.Get("Authorization")
|
|
||||||
if tokenString == "" {
|
|
||||||
if log != nil {
|
|
||||||
log.Warn("Request rejected: missing Authorization header for path: %s", r.URL.Path)
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
if log != nil {
|
|
||||||
log.Warn("Invalid signing method in token: %v", token.Header["alg"])
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
||||||
}
|
|
||||||
return *JWTSecret, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil || !token.Valid {
|
|
||||||
if log != nil {
|
|
||||||
log.Warn("Invalid token: %v", err)
|
|
||||||
}
|
|
||||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the user claims from the token
|
|
||||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
|
||||||
username := claims["username"].(string)
|
|
||||||
// Add the "user" claim to the request context
|
|
||||||
newCtx := context.WithValue(r.Context(), userKey, username)
|
|
||||||
r = r.WithContext(newCtx)
|
|
||||||
|
|
||||||
if log != nil {
|
|
||||||
log.Debug("Authenticated request for user: %s", username)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if log != nil {
|
|
||||||
log.Warn("Invalid token claims structure")
|
|
||||||
}
|
|
||||||
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the next handler in the chain
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
10
boxes.log
10
boxes.log
|
@ -1,10 +0,0 @@
|
||||||
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
|
|
|
@ -1,82 +0,0 @@
|
||||||
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) {
|
|
||||||
log := GetLogger()
|
|
||||||
log.Info("Received %s request to %s", r.Method, r.URL)
|
|
||||||
|
|
||||||
var boxes []Box
|
|
||||||
db.Find(&boxes)
|
|
||||||
json.NewEncoder(w).Encode(boxes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetBoxHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
var box Box
|
|
||||||
if err := db.First(&box, id).Error; err != nil {
|
|
||||||
log.Warn("Box not found with ID: %s", id)
|
|
||||||
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) {
|
|
||||||
log := GetLogger()
|
|
||||||
var box Box
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&box)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to decode box creation request: %v", err)
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("create", fmt.Sprintf("Created box with ID %d", box.ID))
|
|
||||||
json.NewEncoder(w).Encode(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteBoxHandler handles the DELETE /boxes/{id} endpoint.
|
|
||||||
func DeleteBoxHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
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 {
|
|
||||||
log.Warn("Attempt to delete non-existent box with ID: %s", id)
|
|
||||||
http.Error(w, "Box not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the box
|
|
||||||
db.Delete(&box)
|
|
||||||
log.DatabaseAction("delete", fmt.Sprintf("Deleted box with ID %d", box.ID))
|
|
||||||
w.WriteHeader(http.StatusNoContent) // 204 No Content
|
|
||||||
}
|
|
12
config.go
12
config.go
|
@ -16,10 +16,6 @@ type Config struct {
|
||||||
ImageStorageDir string `yaml:"image_storage_dir"`
|
ImageStorageDir string `yaml:"image_storage_dir"`
|
||||||
ListeningPort int `yaml:"listening_port"`
|
ListeningPort int `yaml:"listening_port"`
|
||||||
LogFile string `yaml:"log_file"`
|
LogFile string `yaml:"log_file"`
|
||||||
LogLevel string `yaml:"log_level"`
|
|
||||||
LogOutput string `yaml:"log_output"`
|
|
||||||
StaticFilesDir string `yaml:"static_files_dir"`
|
|
||||||
AllowedOrigins string `yaml:"allowed_origins"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(configPath string) (*Config, error) {
|
func LoadConfig(configPath string) (*Config, error) {
|
||||||
|
@ -41,13 +37,5 @@ func LoadConfig(configPath string) (*Config, error) {
|
||||||
config.DatabasePath = dbPath
|
config.DatabasePath = dbPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.StaticFilesDir == "" {
|
|
||||||
config.StaticFilesDir = "build"
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.AllowedOrigins == "" {
|
|
||||||
config.AllowedOrigins = "http://localhost:8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
database_path: "/app/data/boxes.db"
|
database_path: "data/boxes.db"
|
||||||
test_database_path: "/app/data/test_database.db"
|
test_database_path: "data/test_database.db"
|
||||||
jwt_secret: "super_secret_key"
|
jwt_secret: "super_secret_key"
|
||||||
image_storage_dir: "/app/images"
|
image_storage_dir: "images"
|
||||||
listening_port: 8080
|
listening_port: 8080
|
||||||
log_file: "/app/data/boxes.log"
|
log_file: "boxes.log"
|
||||||
log_level: "INFO"
|
|
||||||
log_output: "both" # Can be "file", "stdout", or "both"
|
|
||||||
static_files_dir: "/app/build/"
|
|
||||||
allowed_origins: "*"
|
|
|
@ -1,8 +0,0 @@
|
||||||
database_path: "./data/boxes.db"
|
|
||||||
test_database_path: "./data/test_database.db"
|
|
||||||
jwt_secret: "super_secret_key"
|
|
||||||
image_storage_dir: "./images/"
|
|
||||||
listening_port: 8080
|
|
||||||
log_file: "boxes.log"
|
|
||||||
static_files_dir: "./build/"
|
|
||||||
allowed_origins: "*"
|
|
23
db.go
23
db.go
|
@ -13,21 +13,6 @@ type Box struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the Tag model
|
|
||||||
type Tag struct {
|
|
||||||
gorm.Model
|
|
||||||
Name string `json:"name" gorm:"uniqueIndex"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Color string `json:"color" gorm:"default:'#808080'"` // Hex color code
|
|
||||||
Items []Item `gorm:"many2many:item_tags;"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ItemTag represents the many-to-many relationship between Items and Tags
|
|
||||||
type ItemTag struct {
|
|
||||||
ItemID uint `gorm:"primaryKey"`
|
|
||||||
TagID uint `gorm:"primaryKey"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the Item model
|
// Define the Item model
|
||||||
type Item struct {
|
type Item struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
|
@ -35,7 +20,6 @@ type Item struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
BoxID uint `json:"box_id"`
|
BoxID uint `json:"box_id"`
|
||||||
ImagePath string `json:"image_path"`
|
ImagePath string `json:"image_path"`
|
||||||
Tags []Tag `json:"tags" gorm:"many2many:item_tags;"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the User model
|
// Define the User model
|
||||||
|
@ -43,7 +27,6 @@ type User struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectDB(dbPath string) (*gorm.DB, error) {
|
func ConnectDB(dbPath string) (*gorm.DB, error) {
|
||||||
|
@ -52,12 +35,8 @@ func ConnectDB(dbPath string) (*gorm.DB, error) {
|
||||||
return nil, fmt.Errorf("failed to connect to database: %v", err)
|
return nil, fmt.Errorf("failed to connect to database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set auto_vacuum mode to ON
|
|
||||||
// this automagically removes old rows from the database when idle
|
|
||||||
db.Exec("PRAGMA auto_vacuum = ON;")
|
|
||||||
|
|
||||||
// AutoMigrate will create the tables if they don't exist
|
// AutoMigrate will create the tables if they don't exist
|
||||||
db.AutoMigrate(&Box{}, &Item{}, &User{}, &Tag{}, &ItemTag{})
|
db.AutoMigrate(&Box{}, &Item{}, &User{})
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
version: '3.3' # Specify the Docker Compose file version
|
|
||||||
|
|
||||||
services:
|
|
||||||
boxes-api:
|
|
||||||
image: localhost/boxes-api:latest # Use the existing boxes-api image
|
|
||||||
#container_name: boxes-api # Name the container
|
|
||||||
ports:
|
|
||||||
- "8080:8080" # Map host port 8080 to container port 8080
|
|
||||||
environment:
|
|
||||||
BOXES_API_CONFIG: "/app/config/config.yaml" # Set the CONFIG environment variable
|
|
||||||
volumes:
|
|
||||||
- /home/stwhite/dockerboxes/boxes-api/data:/app/data # Mount host data directory
|
|
||||||
- /home/stwhite/dockerboxes/boxes-api/images:/app/images # Mount host images directory
|
|
||||||
- /home/stwhite/dockerboxes/boxes-api/config:/app/config # Mount host config directory
|
|
||||||
- /home/stwhite/dockerboxes/boxes-api/build:/app/build
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app-network:
|
|
||||||
driver: bridge
|
|
5
go.mod
5
go.mod
|
@ -7,11 +7,14 @@ require (
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/jinzhu/gorm v1.9.16
|
github.com/jinzhu/gorm v1.9.16
|
||||||
github.com/rs/cors v1.11.1
|
github.com/rs/cors v1.11.1
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.0 // indirect
|
github.com/mattn/go-sqlite3 v1.14.0 // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
11
go.sum
11
go.sum
|
@ -1,5 +1,7 @@
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
|
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/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
|
@ -22,13 +24,16 @@ github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
|
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/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
|
||||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
|
||||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
@ -41,3 +46,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -0,0 +1,404 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginHandler handles the /login endpoint.
|
||||||
|
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req LoginRequest
|
||||||
|
fmt.Println(db, 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 || user.Password != req.Password {
|
||||||
|
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([]byte(config.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// getItemsHandler handles the GET /items endpoint.
|
||||||
|
func GetItemsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var items []Item
|
||||||
|
db.Find(&items)
|
||||||
|
json.NewEncoder(w).Encode(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createItemHandler handles the POST /items endpoint.
|
||||||
|
func CreateItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var item Item
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&item)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(item)
|
||||||
|
|
||||||
|
db.Create(&item)
|
||||||
|
|
||||||
|
// Create a response struct to include the ID
|
||||||
|
type createItemResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
response := createItemResponse{
|
||||||
|
ID: item.ID,
|
||||||
|
Name: item.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadItemImageHandler handles the image upload for an item
|
||||||
|
func UploadItemImageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Extract the authenticated user from context (assuming this is how AuthMiddleware works)
|
||||||
|
user, ok := r.Context().Value(userKey).(string)
|
||||||
|
if !ok || user == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the form data, 10MB limit for file uploads
|
||||||
|
err := r.ParseMultipartForm(10 << 20)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Unable to parse form", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file from the form data
|
||||||
|
file, handler, err := r.FormFile("image")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error retrieving the file", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Get item ID from the URL
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
itemID := vars["id"]
|
||||||
|
|
||||||
|
// Validate that the item exists (fetch from DB using itemID)
|
||||||
|
var item Item
|
||||||
|
if err := db.First(&item, itemID).Error; err != nil {
|
||||||
|
http.Error(w, "Item not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the uploaded file locally or to a storage service
|
||||||
|
// Ensure the directory exists
|
||||||
|
if err := os.MkdirAll(config.ImageStorageDir, 0755); err != nil {
|
||||||
|
http.Error(w, "Unable to create image storage directory", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := fmt.Sprintf("%s/%s", config.ImageStorageDir, handler.Filename)
|
||||||
|
outFile, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Unable to save the file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
// Copy the uploaded file to the destination
|
||||||
|
_, err = io.Copy(outFile, file)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Unable to save the file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the item record in the database with the image path
|
||||||
|
item.ImagePath = filePath
|
||||||
|
if err := db.Save(&item).Error; err != nil {
|
||||||
|
http.Error(w, "Unable to save image path in database", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Image upload called")
|
||||||
|
// Return the image path in the response
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"imagePath": filePath})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetItemImageHandler retrieves an item's image by item ID.
|
||||||
|
func GetItemImageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
itemID := vars["id"]
|
||||||
|
fmt.Println("Getting image")
|
||||||
|
// Retrieve the item from the database
|
||||||
|
var item Item
|
||||||
|
if err := db.First(&item, itemID).Error; err != nil {
|
||||||
|
item.ImagePath = "images/default.jpg"
|
||||||
|
} else if item.ImagePath == "" {
|
||||||
|
item.ImagePath = "images/default.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the image file
|
||||||
|
imageFile, err := os.Open(item.ImagePath)
|
||||||
|
if err != nil {
|
||||||
|
// Log the error for debugging, but don't return an HTTP error
|
||||||
|
fmt.Println("Error opening image.", err)
|
||||||
|
item.ImagePath = "images/default.jpg"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer imageFile.Close()
|
||||||
|
|
||||||
|
// Determine the content type of the image
|
||||||
|
imageData, err := io.ReadAll(imageFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading image")
|
||||||
|
item.ImagePath = "images/default.jpg"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contentType := http.DetectContentType(imageData)
|
||||||
|
|
||||||
|
// Set the content type header and write the image data to the response
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
w.Write(imageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchItemsHandler handles the GET /items/search endpoint.
|
||||||
|
func SearchItemsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query().Get("q")
|
||||||
|
if query == "" {
|
||||||
|
http.Error(w, "Search query is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(query)
|
||||||
|
var items []Item
|
||||||
|
db.Where("name GLOB ? OR description GLOB ?", "*"+query+"*", "*"+query+"*").Find(&items)
|
||||||
|
json.NewEncoder(w).Encode(items)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// getItemHandler handles the GET /items/{id} endpoint.
|
||||||
|
func GetItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
var item Item
|
||||||
|
if err := db.First(&item, id).Error; err != nil {
|
||||||
|
http.Error(w, "Item not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getItemsInBoxHandler handles the GET /boxes/{id}/items endpoint.
|
||||||
|
func GetItemsInBoxHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
var items []Item
|
||||||
|
if err := db.Where("box_id = ?", id).Find(&items).Error; err != nil {
|
||||||
|
http.Error(w, "Items not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateItemHandler handles the PUT /items/{id} endpoint.
|
||||||
|
func UpdateItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
var item Item
|
||||||
|
if err := db.First(&item, id).Error; err != nil {
|
||||||
|
http.Error(w, "Item not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&item)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(item)
|
||||||
|
db.Save(&item)
|
||||||
|
json.NewEncoder(w).Encode(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteItemHandler handles the DELETE /items/{id} endpoint.
|
||||||
|
func DeleteItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
var item Item
|
||||||
|
if err := db.First(&item, id).Error; err != nil {
|
||||||
|
http.Error(w, "Item not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 []byte(config.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)
|
||||||
|
})
|
||||||
|
}
|
242
item_handlers.go
242
item_handlers.go
|
@ -1,242 +0,0 @@
|
||||||
// item_handlers.go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetItemsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
log.Info("Received %s request to %s", r.Method, r.URL)
|
|
||||||
|
|
||||||
var items []Item
|
|
||||||
// Preload tags when getting items
|
|
||||||
if err := db.Preload("Tags").Find(&items).Error; err != nil {
|
|
||||||
log.Error("Failed to fetch items: %v", err)
|
|
||||||
http.Error(w, "Failed to fetch items", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateItemHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
var item Item
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&item)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to decode item creation request: %v", err)
|
|
||||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("Creating new item: %s", item.Name)
|
|
||||||
|
|
||||||
db.Create(&item)
|
|
||||||
|
|
||||||
type createItemResponse struct {
|
|
||||||
ID uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
response := createItemResponse{
|
|
||||||
ID: item.ID,
|
|
||||||
Name: item.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("create", fmt.Sprintf("Created item with ID %d", item.ID))
|
|
||||||
json.NewEncoder(w).Encode(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UploadItemImageHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
user, ok := r.Context().Value(userKey).(string)
|
|
||||||
if !ok || user == "" {
|
|
||||||
log.Warn("Unauthorized image upload attempt")
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.ParseMultipartForm(10 << 20)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to parse multipart form: %v", err)
|
|
||||||
http.Error(w, "Unable to parse form", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file, handler, err := r.FormFile("image")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error retrieving file from form: %v", err)
|
|
||||||
http.Error(w, "Error retrieving the file", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
itemID := vars["id"]
|
|
||||||
|
|
||||||
var item Item
|
|
||||||
if err := db.First(&item, itemID).Error; err != nil {
|
|
||||||
log.Warn("Attempt to upload image for non-existent item with ID: %s", itemID)
|
|
||||||
http.Error(w, "Item not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(*ImageStorage, 0755); err != nil {
|
|
||||||
log.Error("Failed to create image storage directory: %v", err)
|
|
||||||
http.Error(w, "Unable to create image storage directory", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := fmt.Sprintf("%s/%s", *ImageStorage, handler.Filename)
|
|
||||||
outFile, err := os.Create(filePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to create file: %v", err)
|
|
||||||
http.Error(w, "Unable to save the file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer outFile.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(outFile, file)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to save file: %v", err)
|
|
||||||
http.Error(w, "Unable to save the file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
item.ImagePath = filePath
|
|
||||||
if err := db.Save(&item).Error; err != nil {
|
|
||||||
log.Error("Failed to update item with image path: %v", err)
|
|
||||||
http.Error(w, "Unable to save image path in database", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Image uploaded successfully for item %s by user %s", itemID, user)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"imagePath": filePath})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetItemImageHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
itemID := vars["id"]
|
|
||||||
|
|
||||||
var item Item
|
|
||||||
if err := db.First(&item, itemID).Error; err != nil {
|
|
||||||
log.Info("Item not found, using default image for ID: %s", itemID)
|
|
||||||
item.ImagePath = "images/default.jpg"
|
|
||||||
} else if item.ImagePath == "" {
|
|
||||||
item.ImagePath = "images/default.jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
imageFile, err := os.Open(item.ImagePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error opening image file: %v", err)
|
|
||||||
item.ImagePath = "images/default.jpg"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer imageFile.Close()
|
|
||||||
|
|
||||||
imageData, err := io.ReadAll(imageFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error reading image file: %v", err)
|
|
||||||
item.ImagePath = "images/default.jpg"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
contentType := http.DetectContentType(imageData)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
w.Write(imageData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SearchItemsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
query := r.URL.Query().Get("q")
|
|
||||||
if query == "" {
|
|
||||||
log.Warn("Search attempt with empty query")
|
|
||||||
http.Error(w, "Search query is required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Searching for items with query: %s", query)
|
|
||||||
var items []Item
|
|
||||||
db.Where("name GLOB ? OR description GLOB ?", "*"+query+"*", "*"+query+"*").Find(&items)
|
|
||||||
json.NewEncoder(w).Encode(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetItemHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
var item Item
|
|
||||||
if err := db.Preload("Tags").First(&item, id).Error; err != nil {
|
|
||||||
log.Warn("Item not found with ID: %s", id)
|
|
||||||
http.Error(w, "Item not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetItemsInBoxHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
var items []Item
|
|
||||||
if err := db.Preload("Tags").Where("box_id = ?", id).Find(&items).Error; err != nil {
|
|
||||||
log.Warn("Failed to fetch items for box ID: %s", id)
|
|
||||||
http.Error(w, "Items not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Retrieved %d items from box %s", len(items), id)
|
|
||||||
json.NewEncoder(w).Encode(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateItemHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
var item Item
|
|
||||||
if err := db.First(&item, id).Error; err != nil {
|
|
||||||
log.Warn("Attempt to update non-existent item with ID: %s", id)
|
|
||||||
http.Error(w, "Item not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&item)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to decode item update request: %v", err)
|
|
||||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("update", fmt.Sprintf("Updated item with ID %s", id))
|
|
||||||
db.Save(&item)
|
|
||||||
json.NewEncoder(w).Encode(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteItemHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
var item Item
|
|
||||||
if err := db.First(&item, id).Error; err != nil {
|
|
||||||
log.Warn("Attempt to delete non-existent item with ID: %s", id)
|
|
||||||
http.Error(w, "Item not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("delete", fmt.Sprintf("Deleted item with ID %s", id))
|
|
||||||
db.Delete(&item)
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
213
logger.go
213
logger.go
|
@ -1,213 +0,0 @@
|
||||||
// logger.go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger struct {
|
|
||||||
*log.Logger
|
|
||||||
writers []io.Writer
|
|
||||||
level LogLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogLevel int
|
|
||||||
|
|
||||||
const (
|
|
||||||
DEBUG LogLevel = iota
|
|
||||||
INFO
|
|
||||||
WARN
|
|
||||||
ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
instance *Logger
|
|
||||||
// Map for converting string to LogLevel
|
|
||||||
logLevelMap = map[string]LogLevel{
|
|
||||||
"DEBUG": DEBUG,
|
|
||||||
"INFO": INFO,
|
|
||||||
"WARN": WARN,
|
|
||||||
"ERROR": ERROR,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize creates a new logger instance with specified configuration
|
|
||||||
func Initialize(logPath string, logOutput string) (*Logger, error) {
|
|
||||||
if instance != nil {
|
|
||||||
return instance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var writers []io.Writer
|
|
||||||
|
|
||||||
// Handle different output configurations
|
|
||||||
switch logOutput {
|
|
||||||
case "stdout":
|
|
||||||
writers = append(writers, os.Stdout)
|
|
||||||
case "file":
|
|
||||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open log file: %v", err)
|
|
||||||
}
|
|
||||||
writers = append(writers, file)
|
|
||||||
case "both":
|
|
||||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open log file: %v", err)
|
|
||||||
}
|
|
||||||
writers = append(writers, file, os.Stdout)
|
|
||||||
default:
|
|
||||||
// Default to stdout if invalid option provided
|
|
||||||
writers = append(writers, os.Stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create multi-writer if we have multiple writers
|
|
||||||
var writer io.Writer
|
|
||||||
if len(writers) > 1 {
|
|
||||||
writer = io.MultiWriter(writers...)
|
|
||||||
} else {
|
|
||||||
writer = writers[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create logger instance
|
|
||||||
logger := &Logger{
|
|
||||||
Logger: log.New(writer, "", 0),
|
|
||||||
writers: writers,
|
|
||||||
level: INFO, // Default to INFO level
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = logger
|
|
||||||
return logger, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLogLevel sets the logging level from a string
|
|
||||||
func (l *Logger) SetLogLevel(levelStr string) error {
|
|
||||||
level, ok := logLevelMap[levelStr]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid log level: %s. Must be one of: DEBUG, INFO, WARN, ERROR", levelStr)
|
|
||||||
}
|
|
||||||
l.level = level
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogLevel returns the current logging level as a string
|
|
||||||
func (l *Logger) GetLogLevel() string {
|
|
||||||
for str, level := range logLevelMap {
|
|
||||||
if level == l.level {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "UNKNOWN"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogger returns the singleton logger instance
|
|
||||||
func GetLogger() *Logger {
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes all writers that implement io.Closer
|
|
||||||
func (l *Logger) Close() error {
|
|
||||||
var errs []error
|
|
||||||
for _, writer := range l.writers {
|
|
||||||
if closer, ok := writer.(io.Closer); ok {
|
|
||||||
if err := closer.Close(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return fmt.Errorf("errors closing writers: %v", errs)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) log(level LogLevel, format string, v ...interface{}) {
|
|
||||||
if l == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if level < l.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get caller information
|
|
||||||
_, file, line, _ := runtime.Caller(2)
|
|
||||||
|
|
||||||
// Create timestamp
|
|
||||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
// Create level string
|
|
||||||
levelStr := "INFO"
|
|
||||||
switch level {
|
|
||||||
case DEBUG:
|
|
||||||
levelStr = "DEBUG"
|
|
||||||
case WARN:
|
|
||||||
levelStr = "WARN"
|
|
||||||
case ERROR:
|
|
||||||
levelStr = "ERROR"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format message
|
|
||||||
message := fmt.Sprintf(format, v...)
|
|
||||||
|
|
||||||
// Final log format
|
|
||||||
logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s\n",
|
|
||||||
timestamp,
|
|
||||||
levelStr,
|
|
||||||
file,
|
|
||||||
line,
|
|
||||||
message,
|
|
||||||
)
|
|
||||||
|
|
||||||
l.Logger.Print(logLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logs a debug message
|
|
||||||
func (l *Logger) Debug(format string, v ...interface{}) {
|
|
||||||
l.log(DEBUG, format, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info logs an info message
|
|
||||||
func (l *Logger) Info(format string, v ...interface{}) {
|
|
||||||
l.log(INFO, format, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn logs a warning message
|
|
||||||
func (l *Logger) Warn(format string, v ...interface{}) {
|
|
||||||
l.log(WARN, format, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error logs an error message
|
|
||||||
func (l *Logger) Error(format string, v ...interface{}) {
|
|
||||||
l.log(ERROR, format, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPRequest logs an HTTP request
|
|
||||||
func (l *Logger) HTTPRequest(method, path, username string, statusCode int) {
|
|
||||||
l.log(INFO, "HTTP %s %s - User: %s - Status: %d",
|
|
||||||
method,
|
|
||||||
path,
|
|
||||||
username,
|
|
||||||
statusCode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserAction logs user actions like login, logout, etc.
|
|
||||||
func (l *Logger) UserAction(username, action string) {
|
|
||||||
l.log(INFO, "User Action - Username: %s - Action: %s",
|
|
||||||
username,
|
|
||||||
action,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DatabaseAction logs database operations
|
|
||||||
func (l *Logger) DatabaseAction(operation, details string) {
|
|
||||||
l.log(INFO, "Database Action - Operation: %s - Details: %s",
|
|
||||||
operation,
|
|
||||||
details,
|
|
||||||
)
|
|
||||||
}
|
|
354
main.go
354
main.go
|
@ -5,339 +5,75 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Global variables
|
|
||||||
var (
|
var (
|
||||||
db *gorm.DB
|
db *gorm.DB // Declare db globally
|
||||||
config *Config
|
config *Config
|
||||||
JWTSecret *[]byte
|
|
||||||
ImageStorage *string
|
|
||||||
DatabasePath *string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load configuration first
|
|
||||||
|
configFile := os.Getenv("CONFIG")
|
||||||
var err error
|
var err error
|
||||||
config, err = loadAndValidateConfig()
|
config, err = LoadConfig(configFile)
|
||||||
if err != nil {
|
|
||||||
|
// check for errors
|
||||||
|
if err != nil || config == nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize logger with config values
|
fmt.Println(config.DatabasePath)
|
||||||
logger, err := Initialize(config.LogFile, "both") // "both" means log to both file and stdout
|
fmt.Println(config.ImageStorageDir)
|
||||||
if err != nil {
|
fmt.Println(config.JWTSecret)
|
||||||
log.Fatalf("Failed to initialize logger: %v", err)
|
fmt.Println(config.LogFile)
|
||||||
}
|
fmt.Println(config.ListeningPort)
|
||||||
defer logger.Close()
|
|
||||||
|
|
||||||
// Set up logging configuration
|
// Connect to the database
|
||||||
if err := setupLogging(config.LogLevel); err != nil {
|
db, err = ConnectDB(config.DatabasePath)
|
||||||
logger.Error("Failed to set up logging: %v", err)
|
if err != nil || db == nil {
|
||||||
os.Exit(1)
|
log.Fatalf("Failed to connect to database: %v", err)
|
||||||
}
|
|
||||||
|
|
||||||
// Log configuration details
|
|
||||||
logConfigDetails(config)
|
|
||||||
|
|
||||||
// Connect to database
|
|
||||||
db, err = connectToDatabase(config)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Failed to connect to database: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// Create and configure the router
|
fmt.Println("Default user 'boxuser' created successfully!")
|
||||||
router := createRouter()
|
|
||||||
|
|
||||||
// Create CORS handler
|
// Create the router
|
||||||
corsHandler := createCORSHandler(config.AllowedOrigins)
|
router := mux.NewRouter()
|
||||||
|
|
||||||
// Start the server
|
// Define your routes
|
||||||
startServer(config.ListeningPort, corsHandler(router))
|
|
||||||
}
|
|
||||||
|
|
||||||
func createRouter() *mux.Router {
|
|
||||||
baseRouter := mux.NewRouter()
|
|
||||||
baseRouter.Use(loggingMiddleware)
|
|
||||||
|
|
||||||
// API routes should be registered first with a strict prefix match
|
|
||||||
apiRouter := baseRouter.PathPrefix("/api/v1").Subrouter()
|
|
||||||
apiRouter.StrictSlash(true) // This ensures /api/v1/ and /api/v1 are treated the same
|
|
||||||
setupAPIRoutes(apiRouter)
|
|
||||||
|
|
||||||
// Static file serving should be last and only match if no API routes matched
|
|
||||||
staticRouter := baseRouter.NewRoute().Subrouter()
|
|
||||||
setupStaticRoutes(staticRouter, config.StaticFilesDir)
|
|
||||||
|
|
||||||
// Add a catch-all NotFoundHandler to the base router
|
|
||||||
baseRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// If the path starts with /api, return a 404 API error
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
|
||||||
http.Error(w, "API endpoint not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Otherwise serve the index.html for client-side routing
|
|
||||||
http.ServeFile(w, r, filepath.Join(config.StaticFilesDir, "index.html"))
|
|
||||||
})
|
|
||||||
|
|
||||||
return baseRouter
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupAPIRoutes(router *mux.Router) {
|
|
||||||
// Public routes
|
|
||||||
router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS")
|
router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS")
|
||||||
|
router.Handle("/boxes", AuthMiddleware(http.HandlerFunc(GetBoxesHandler))).Methods("GET", "OPTIONS")
|
||||||
|
router.Handle("/boxes", AuthMiddleware(http.HandlerFunc(CreateBoxHandler))).Methods("POST", "OPTIONS")
|
||||||
|
router.Handle("/boxes/{id}", AuthMiddleware(http.HandlerFunc(DeleteBoxHandler))).Methods("DELETE", "OPTIONS")
|
||||||
|
router.Handle("/boxes/{id}", AuthMiddleware(http.HandlerFunc(GetBoxHandler))).Methods("GET", "OPTIONS")
|
||||||
|
router.Handle("/items", AuthMiddleware(http.HandlerFunc(GetItemsHandler))).Methods("GET", "OPTIONS")
|
||||||
|
router.Handle("/items", AuthMiddleware(http.HandlerFunc(CreateItemHandler))).Methods("POST", "OPTIONS")
|
||||||
|
router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(GetItemHandler))).Methods("GET", "OPTIONS")
|
||||||
|
router.Handle("/boxes/{id}/items", AuthMiddleware(http.HandlerFunc(GetItemsInBoxHandler))).Methods("GET", "OPTIONS")
|
||||||
|
router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(UpdateItemHandler))).Methods("PUT", "OPTIONS")
|
||||||
|
router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(DeleteItemHandler))).Methods("DELETE", "OPTIONS")
|
||||||
|
router.Handle("/items/{id}/image", AuthMiddleware(http.HandlerFunc(GetItemImageHandler))).Methods("GET", "OPTIONS")
|
||||||
|
fmt.Println("Registering route for search items...")
|
||||||
|
router.Handle("/search/items", AuthMiddleware(http.HandlerFunc(SearchItemsHandler))).Methods("GET", "OPTIONS")
|
||||||
|
// Add a new route for uploading an image with AuthMiddleware
|
||||||
|
router.HandleFunc("/items/{id}/upload", UploadItemImageHandler).
|
||||||
|
Methods("POST").
|
||||||
|
Handler(AuthMiddleware(http.HandlerFunc(UploadItemImageHandler)))
|
||||||
|
|
||||||
// Protected routes
|
// Apply CORS middleware
|
||||||
protected := router.NewRoute().Subrouter()
|
c := cors.New(cors.Options{
|
||||||
protected.Use(AuthMiddleware)
|
AllowedOrigins: []string{"http://localhost:3000", "http://10.0.0.16:3000"}, // Change this to your frontend domain
|
||||||
|
|
||||||
// Box routes
|
|
||||||
protected.Handle("/boxes", http.HandlerFunc(GetBoxesHandler)).Methods("GET", "OPTIONS")
|
|
||||||
protected.Handle("/boxes", http.HandlerFunc(CreateBoxHandler)).Methods("POST", "OPTIONS")
|
|
||||||
protected.Handle("/boxes/{id}", http.HandlerFunc(GetBoxHandler)).Methods("GET", "OPTIONS")
|
|
||||||
protected.Handle("/boxes/{id}", http.HandlerFunc(DeleteBoxHandler)).Methods("DELETE", "OPTIONS")
|
|
||||||
protected.Handle("/boxes/{id}/items", http.HandlerFunc(GetItemsInBoxHandler)).Methods("GET", "OPTIONS")
|
|
||||||
|
|
||||||
// Item routes
|
|
||||||
protected.Handle("/items", http.HandlerFunc(GetItemsHandler)).Methods("GET", "OPTIONS")
|
|
||||||
protected.Handle("/items", http.HandlerFunc(CreateItemHandler)).Methods("POST", "OPTIONS")
|
|
||||||
protected.Handle("/items/{id}", http.HandlerFunc(GetItemHandler)).Methods("GET", "OPTIONS")
|
|
||||||
protected.Handle("/items/{id}", http.HandlerFunc(UpdateItemHandler)).Methods("PUT", "OPTIONS")
|
|
||||||
protected.Handle("/items/{id}", http.HandlerFunc(DeleteItemHandler)).Methods("DELETE", "OPTIONS")
|
|
||||||
protected.Handle("/items/{id}/image", http.HandlerFunc(GetItemImageHandler)).Methods("GET", "OPTIONS")
|
|
||||||
protected.Handle("/items/{id}/upload", http.HandlerFunc(UploadItemImageHandler)).Methods("POST", "OPTIONS")
|
|
||||||
protected.Handle("/search/items", http.HandlerFunc(SearchItemsHandler)).Methods("GET", "OPTIONS")
|
|
||||||
|
|
||||||
// Tag routes
|
|
||||||
protected.Handle("/tags", http.HandlerFunc(GetTagsHandler)).Methods("GET", "OPTIONS")
|
|
||||||
protected.Handle("/tags", http.HandlerFunc(CreateTagHandler)).Methods("POST", "OPTIONS")
|
|
||||||
protected.Handle("/tags/{id}", http.HandlerFunc(UpdateTagHandler)).Methods("PUT", "OPTIONS")
|
|
||||||
protected.Handle("/tags/{id}", http.HandlerFunc(DeleteTagHandler)).Methods("DELETE", "OPTIONS")
|
|
||||||
protected.Handle("/tags/{id}/items", http.HandlerFunc(GetItemsByTagHandler)).Methods("GET", "OPTIONS")
|
|
||||||
|
|
||||||
// Item-Tag relationship routes
|
|
||||||
protected.Handle("/items/{id}/tags", http.HandlerFunc(AddItemTagsHandler)).Methods("POST", "OPTIONS")
|
|
||||||
protected.Handle("/items/{id}/tags/{tagId}", http.HandlerFunc(RemoveItemTagHandler)).Methods("DELETE", "OPTIONS")
|
|
||||||
|
|
||||||
// Admin routes
|
|
||||||
adminRouter := protected.PathPrefix("/admin").Subrouter()
|
|
||||||
setupManagementRoutes(adminRouter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupManagementRoutes(router *mux.Router) {
|
|
||||||
router.Handle("/user", http.HandlerFunc(GetUsersHandler)).Methods("GET", "OPTIONS")
|
|
||||||
router.Handle("/user", http.HandlerFunc(CreateUserHandler)).Methods("POST", "OPTIONS")
|
|
||||||
router.Handle("/user/{id}", http.HandlerFunc(GetUserHandler)).Methods("GET", "OPTIONS")
|
|
||||||
router.Handle("/user/{id}", http.HandlerFunc(DeleteUserHandler)).Methods("DELETE", "OPTIONS")
|
|
||||||
router.Handle("/db", http.HandlerFunc(BackupDatabaseHandler)).Methods("GET", "OPTIONS")
|
|
||||||
router.Handle("/db", http.HandlerFunc(RestoreDatabaseHandler)).Methods("POST", "OPTIONS")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupStaticRoutes(router *mux.Router, staticPath string) {
|
|
||||||
// Ensure static directory exists
|
|
||||||
if err := validateStaticDirectory(staticPath); err != nil {
|
|
||||||
log.Printf("Warning: Static directory validation failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileServer := http.FileServer(http.Dir(staticPath))
|
|
||||||
|
|
||||||
// Only serve static files for paths that don't start with /api
|
|
||||||
router.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := GetLogger()
|
|
||||||
|
|
||||||
// Don't serve static files for API paths
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log static file request
|
|
||||||
if logger != nil {
|
|
||||||
logger.Debug("Static file request: %s", r.URL.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the file exists
|
|
||||||
path := filepath.Join(staticPath, r.URL.Path)
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
|
|
||||||
// If file doesn't exist, serve index.html for SPA routing
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Debug("File not found, serving index.html: %s", path)
|
|
||||||
}
|
|
||||||
http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip the leading "/" before serving
|
|
||||||
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func createCustomStaticHandler(staticPath string) http.HandlerFunc {
|
|
||||||
fileServer := http.FileServer(http.Dir(staticPath))
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := GetLogger()
|
|
||||||
|
|
||||||
// Log static file request
|
|
||||||
if logger != nil {
|
|
||||||
logger.Debug("Static file request: %s", r.URL.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the file exists
|
|
||||||
path := filepath.Join(staticPath, r.URL.Path)
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
|
|
||||||
// If file doesn't exist, serve index.html for SPA routing
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Debug("File not found, serving index.html: %s", path)
|
|
||||||
}
|
|
||||||
http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileServer.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadAndValidateConfig() (*Config, error) {
|
|
||||||
configFile := os.Getenv("BOXES_API_CONFIG")
|
|
||||||
if configFile == "" {
|
|
||||||
configFile = "config.yaml" // Default config path
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
config, err := LoadConfig(configFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set global variables
|
|
||||||
jwtSecretBytes := []byte(config.JWTSecret)
|
|
||||||
JWTSecret = &jwtSecretBytes
|
|
||||||
ImageStorage = &config.ImageStorageDir
|
|
||||||
DatabasePath = &config.DatabasePath
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupLogging(logLevel string) error {
|
|
||||||
logger := GetLogger()
|
|
||||||
if logger == nil {
|
|
||||||
return fmt.Errorf("logger not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if logLevel != "" {
|
|
||||||
if err := logger.SetLogLevel(logLevel); err != nil {
|
|
||||||
return fmt.Errorf("failed to set log level: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logConfigDetails(config *Config) {
|
|
||||||
logger := GetLogger()
|
|
||||||
if logger == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Configuration loaded:")
|
|
||||||
logger.Info("Database Path: %s", config.DatabasePath)
|
|
||||||
logger.Info("Image Storage Dir: %s", config.ImageStorageDir)
|
|
||||||
logger.Info("Log File: %s", config.LogFile)
|
|
||||||
logger.Info("Log Level: %s", config.LogLevel)
|
|
||||||
logger.Info("Listening Port: %d", config.ListeningPort)
|
|
||||||
logger.Info("Allowed Origins: %s", config.AllowedOrigins)
|
|
||||||
logger.Info("Static Files Dir: %s", config.StaticFilesDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func connectToDatabase(config *Config) (*gorm.DB, error) {
|
|
||||||
if config == nil {
|
|
||||||
return nil, fmt.Errorf("config is nil")
|
|
||||||
}
|
|
||||||
return ConnectDB(config.DatabasePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createCORSHandler(allowedOrigins string) func(http.Handler) http.Handler {
|
|
||||||
corsOpts := cors.Options{
|
|
||||||
AllowedOrigins: []string{allowedOrigins},
|
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
AllowedHeaders: []string{"Authorization", "Content-Type"},
|
AllowedHeaders: []string{"Authorization", "Content-Type"},
|
||||||
ExposedHeaders: []string{"Content-Length"},
|
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
}
|
|
||||||
|
|
||||||
// If allowedOrigins is "*", allow all origins
|
|
||||||
if allowedOrigins == "*" {
|
|
||||||
corsOpts.AllowedOrigins = []string{"*"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cors.New(corsOpts).Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func startServer(port int, handler http.Handler) {
|
|
||||||
logger := GetLogger()
|
|
||||||
addr := fmt.Sprintf(":%d", port)
|
|
||||||
|
|
||||||
if logger != nil {
|
|
||||||
logger.Info("Server starting on port %d", port)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := http.ListenAndServe(addr, handler); err != nil {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Error("Server failed to start: %v", err)
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateStaticDirectory(path string) error {
|
|
||||||
if info, err := os.Stat(path); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("static directory does not exist: %s", path)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("error accessing static directory: %v", err)
|
|
||||||
} else if !info.IsDir() {
|
|
||||||
return fmt.Errorf("static path is not a directory: %s", path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type responseWriter struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *responseWriter) WriteHeader(code int) {
|
|
||||||
rw.statusCode = code
|
|
||||||
rw.ResponseWriter.WriteHeader(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loggingMiddleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := GetLogger()
|
|
||||||
|
|
||||||
username := "anonymous"
|
|
||||||
if user, ok := r.Context().Value(userKey).(string); ok {
|
|
||||||
username = user
|
|
||||||
}
|
|
||||||
|
|
||||||
rw := &responseWriter{w, http.StatusOK}
|
|
||||||
|
|
||||||
next.ServeHTTP(rw, r)
|
|
||||||
|
|
||||||
if logger != nil {
|
|
||||||
logger.HTTPRequest(r.Method, r.URL.Path, username, rw.statusCode)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Start the server with CORS middleware
|
||||||
|
fmt.Printf("Server listening on port %d\n", config.ListeningPort)
|
||||||
|
http.ListenAndServe(fmt.Sprintf(":%d", config.ListeningPort), c.Handler(router))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
|
|
||||||
podman run -d \
|
|
||||||
--name boxes-api \
|
|
||||||
-p 8080:8080 \
|
|
||||||
-e BOXES_API_CONFIG=/app/config/config.yaml \
|
|
||||||
-v ./data:/app/data \
|
|
||||||
-v ./images:/app/images \
|
|
||||||
-v ./config:/app/config \
|
|
||||||
-v ./build:/app/build \
|
|
||||||
--network boxes_app-network \
|
|
||||||
localhost/boxes-api:latest
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080/api/v1"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
# Function to make an authenticated request
|
|
||||||
function authenticated_request() {
|
|
||||||
local method=$1
|
|
||||||
local endpoint=$2
|
|
||||||
local data=$3
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
|
|
||||||
# Make the authenticated request
|
|
||||||
curl -s -X $method -H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$data" \
|
|
||||||
"$API_BASE_URL$endpoint"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get all boxes
|
|
||||||
response=$(authenticated_request "GET" "/tags/1/items" "")
|
|
||||||
|
|
||||||
echo $response
|
|
||||||
|
|
||||||
# Check if the request was successful
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
# Pretty print the boxes using jq
|
|
||||||
echo "$response" | jq .
|
|
||||||
else
|
|
||||||
echo "Error: Failed to get boxes."
|
|
||||||
fi
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
|
|
||||||
curl -X GET \
|
|
||||||
$API_BASE_URL/admin/db \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
--output ./test.db
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/binb/ash
|
|
||||||
|
|
||||||
# Set the password you want to use
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
DATABASE="/Users/stwhite/CODE/Boxes-App/boxes-api/data/boxes.db"
|
|
||||||
=======
|
|
||||||
DATABASE="../data/boxes.db"
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
|
|
||||||
# Use htpasswd to create a bcrypt hash of the password
|
|
||||||
HASHED_PASSWORD=$(htpasswd -nbBC 10 "" "$PASSWORD" | tr -d ':\n')
|
|
||||||
|
|
||||||
# Create the user in the SQLite database
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
sqlite3 $DATABASE "INSERT INTO users (username, password, email) VALUES ('boxuser', '$HASHED_PASSWORD','boxuser@r8z.us')"
|
|
||||||
=======
|
|
||||||
sqlite3 $DATABASE "INSERT INTO users (username, password, email) VALUES ('boxuser', '$HASHED_PASSWORD','boxuser@r8z.us');"
|
|
||||||
>>>>>>> Stashed changes
|
|
|
@ -1,23 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
JSON_PAYLOAD='{
|
|
||||||
"username": "testuser",
|
|
||||||
"password": "testuser"
|
|
||||||
}'
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
|
|
||||||
curl -X DELETE \
|
|
||||||
$API_BASE_URL/admin/user/2 \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
|
@ -1,70 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080/api/v1"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
# Usage message
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 -m <method> -e <endpoint> [-i <id>] [-d <data>]"
|
|
||||||
echo " -m <method> HTTP method (GET, POST, PUT, DELETE)"
|
|
||||||
echo " -e <endpoint> API endpoint (e.g., /tags/1/items)"
|
|
||||||
echo " -i <id> Optional element ID (appended to endpoint)"
|
|
||||||
echo " -d <data> Optional data (for POST/PUT requests)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse command line options
|
|
||||||
while getopts ":m:e:i:d:" opt; do
|
|
||||||
case $opt in
|
|
||||||
m) method="$OPTARG" ;;
|
|
||||||
e) endpoint="$OPTARG" ;;
|
|
||||||
i) id="$OPTARG" ;;
|
|
||||||
d) data="$OPTARG" ;;
|
|
||||||
\?) usage ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check if required options are provided
|
|
||||||
if [[ -z "$method" || -z "$endpoint" ]]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Construct the URL
|
|
||||||
url="$API_BASE_URL$endpoint"
|
|
||||||
if [[ -n "$id" ]]; then
|
|
||||||
url="$url/$id"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Function to make an authenticated request
|
|
||||||
function authenticated_request() {
|
|
||||||
local method=$1
|
|
||||||
local url=$2
|
|
||||||
local data=$3
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
|
|
||||||
# Make the authenticated request
|
|
||||||
curl -s -X "$method" -H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$data" \
|
|
||||||
"$url"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Make the API request
|
|
||||||
response=$(authenticated_request "$method" "$url" "$data")
|
|
||||||
echo "Raw response: $response"
|
|
||||||
|
|
||||||
# Check if the request was successful
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
# Pretty print the response using jq
|
|
||||||
echo "$response" | jq .
|
|
||||||
else
|
|
||||||
echo "Error: Failed to make API request."
|
|
||||||
fi
|
|
|
@ -1,31 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
API_BASE_URL="http://localhost:8080/api/v1" # Adjusted to include /api/v1
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
echo "Sending request to: $API_BASE_URL/login"
|
|
||||||
echo "Username: $USERNAME"
|
|
||||||
echo "Password: $PASSWORD"
|
|
||||||
|
|
||||||
response=$(curl -vi -w "\n%{http_code}" -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login")
|
|
||||||
|
|
||||||
http_status=$(echo "$response" | tail -n1)
|
|
||||||
body=$(echo "$response" | sed '$d')
|
|
||||||
|
|
||||||
echo "HTTP Status: $http_status"
|
|
||||||
echo "Response body: $body"
|
|
||||||
|
|
||||||
if [ "$http_status" -eq 200 ]; then
|
|
||||||
TOKEN=$(echo "$body" | jq -r '.token // empty')
|
|
||||||
if [ -n "$TOKEN" ]; then
|
|
||||||
echo "Token obtained successfully:"
|
|
||||||
echo "$TOKEN"
|
|
||||||
else
|
|
||||||
echo "Failed to extract token from response."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Failed to obtain token. Server returned status $http_status"
|
|
||||||
fi
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# API base URL
|
# API base URL
|
||||||
API_BASE_URL="http://localhost:8080/api/v1"
|
API_BASE_URL="http://10.0.0.66:8080"
|
||||||
|
|
||||||
# Login credentials
|
# Login credentials
|
||||||
USERNAME="boxuser"
|
USERNAME="boxuser"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# API base URL
|
# API base URL
|
||||||
API_BASE_URL="http://localhost:8080/api/v1"
|
API_BASE_URL="http://localhost:8080"
|
||||||
|
|
||||||
# Login credentials
|
# Login credentials
|
||||||
USERNAME="boxuser"
|
USERNAME="boxuser"
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080/api/v1"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
# Function to make an authenticated request
|
|
||||||
function authenticated_request() {
|
|
||||||
local method=$1
|
|
||||||
local endpoint=$2
|
|
||||||
local data=$3
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
|
|
||||||
# Make the authenticated request
|
|
||||||
curl -s -X $method -H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$data" \
|
|
||||||
"$API_BASE_URL$endpoint"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get all boxes
|
|
||||||
response=$(authenticated_request "GET" "/tags/1/items" "")
|
|
||||||
|
|
||||||
echo $response
|
|
||||||
|
|
||||||
# Check if the request was successful
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
# Pretty print the boxes using jq
|
|
||||||
echo "$response" | jq .
|
|
||||||
else
|
|
||||||
echo "Error: Failed to get boxes."
|
|
||||||
fi
|
|
|
@ -1,39 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080/api/v1"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
# Function to make an authenticated request
|
|
||||||
function authenticated_request() {
|
|
||||||
local method=$1
|
|
||||||
local endpoint=$2
|
|
||||||
local data=$3
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
|
|
||||||
# Make the authenticated request
|
|
||||||
curl -s -X $method -H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$data" \
|
|
||||||
"$API_BASE_URL$endpoint"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get all boxes
|
|
||||||
response=$(authenticated_request "GET" "/tags?include_items=true" "")
|
|
||||||
|
|
||||||
echo $response
|
|
||||||
|
|
||||||
# Check if the request was successful
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
# Pretty print the boxes using jq
|
|
||||||
echo "$response" | jq .
|
|
||||||
else
|
|
||||||
echo "Error: Failed to get boxes."
|
|
||||||
fi
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080/api/v1/"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
echo $TOKEN
|
|
||||||
|
|
||||||
curl -X GET \
|
|
||||||
$API_BASE_URL/admin/user \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json"
|
|
|
@ -1,24 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
JSON_PAYLOAD='{
|
|
||||||
"username": "testuser",
|
|
||||||
"password": "testuser"
|
|
||||||
}'
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
|
|
||||||
curl -X POST \
|
|
||||||
$API_BASE_URL/admin/user \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$JSON_PAYLOAD"
|
|
|
@ -1,18 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# API base URL
|
|
||||||
API_BASE_URL="http://localhost:8080"
|
|
||||||
|
|
||||||
# Login credentials
|
|
||||||
USERNAME="boxuser"
|
|
||||||
PASSWORD="boxuser"
|
|
||||||
|
|
||||||
# Get a new JWT token
|
|
||||||
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
|
||||||
"$API_BASE_URL/login" | jq -r '.token')
|
|
||||||
|
|
||||||
curl -X POST \
|
|
||||||
$API_BASE_URL/admin/db \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-F "database=@./test.db"
|
|
319
tags_handlers.go
319
tags_handlers.go
|
@ -1,319 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TagRequest represents the request body for tag operations
|
|
||||||
type TagRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTagsHandler returns all tags
|
|
||||||
func GetTagsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
loadItems := r.URL.Query().Get("include_items") == "true"
|
|
||||||
|
|
||||||
var tags []Tag
|
|
||||||
query := db.Model(&Tag{})
|
|
||||||
|
|
||||||
// Only preload items if specifically requested
|
|
||||||
if loadItems {
|
|
||||||
query = query.Preload("Items")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Find(&tags).Error; err != nil {
|
|
||||||
log.Error("Failed to fetch tags: %v", err)
|
|
||||||
http.Error(w, "Failed to fetch tags", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTagHandler creates a new tag
|
|
||||||
func CreateTagHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
var req TagRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
log.Error("Failed to decode tag creation request: %v", err)
|
|
||||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate tag name
|
|
||||||
req.Name = strings.TrimSpace(req.Name)
|
|
||||||
if req.Name == "" {
|
|
||||||
http.Error(w, "Tag name is required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate tag name
|
|
||||||
var existingTag Tag
|
|
||||||
if err := db.Where("name = ?", req.Name).First(&existingTag).Error; err == nil {
|
|
||||||
http.Error(w, "Tag name already exists", http.StatusConflict)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tag := Tag{
|
|
||||||
Name: req.Name,
|
|
||||||
Description: req.Description,
|
|
||||||
Color: req.Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Create(&tag).Error; err != nil {
|
|
||||||
log.Error("Failed to create tag: %v", err)
|
|
||||||
http.Error(w, "Failed to create tag", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("create", fmt.Sprintf("Created tag: %s", tag.Name))
|
|
||||||
json.NewEncoder(w).Encode(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateTagHandler updates an existing tag
|
|
||||||
func UpdateTagHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
var tag Tag
|
|
||||||
if err := db.First(&tag, id).Error; err != nil {
|
|
||||||
http.Error(w, "Tag not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req TagRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate name if name is being changed
|
|
||||||
if req.Name != tag.Name {
|
|
||||||
var existingTag Tag
|
|
||||||
if err := db.Where("name = ? AND id != ?", req.Name, id).First(&existingTag).Error; err == nil {
|
|
||||||
http.Error(w, "Tag name already exists", http.StatusConflict)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.Name = req.Name
|
|
||||||
tag.Description = req.Description
|
|
||||||
tag.Color = req.Color
|
|
||||||
|
|
||||||
if err := db.Save(&tag).Error; err != nil {
|
|
||||||
log.Error("Failed to update tag: %v", err)
|
|
||||||
http.Error(w, "Failed to update tag", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("update", fmt.Sprintf("Updated tag: %s", tag.Name))
|
|
||||||
json.NewEncoder(w).Encode(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTagHandler deletes a tag
|
|
||||||
func DeleteTagHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
// Begin transaction
|
|
||||||
tx := db.Begin()
|
|
||||||
|
|
||||||
// Remove tag associations
|
|
||||||
if err := tx.Table("item_tags").Where("tag_id = ?", id).Delete(ItemTag{}).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.Error("Failed to remove tag associations: %v", err)
|
|
||||||
http.Error(w, "Failed to delete tag", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the tag
|
|
||||||
if err := tx.Delete(&Tag{}, id).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.Error("Failed to delete tag: %v", err)
|
|
||||||
http.Error(w, "Failed to delete tag", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit transaction
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
log.Error("Failed to commit tag deletion: %v", err)
|
|
||||||
http.Error(w, "Failed to delete tag", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("delete", fmt.Sprintf("Deleted tag ID: %s", id))
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddItemTagsHandler adds tags to an item
|
|
||||||
// AddItemTagsHandler adds tags to an item
|
|
||||||
func AddItemTagsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
itemID := vars["id"]
|
|
||||||
|
|
||||||
log.Info("Attempting to add tags to item %s", itemID)
|
|
||||||
|
|
||||||
var item Item
|
|
||||||
if err := db.First(&item, itemID).Error; err != nil {
|
|
||||||
log.Error("Failed to find item %s: %v", itemID, err)
|
|
||||||
http.Error(w, "Item not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var tagIDs []uint
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&tagIDs); err != nil {
|
|
||||||
log.Error("Failed to decode tag IDs from request: %v", err)
|
|
||||||
http.Error(w, "Invalid request body - expecting array of tag IDs", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Received request to add tags %v to item %s", tagIDs, itemID)
|
|
||||||
|
|
||||||
// Begin transaction
|
|
||||||
tx := db.Begin()
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// First, fetch all the tags we want to add
|
|
||||||
var tags []Tag
|
|
||||||
if err := tx.Where("id IN (?)", tagIDs).Find(&tags).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.Error("Failed to fetch tags: %v", err)
|
|
||||||
http.Error(w, "Failed to fetch tags", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify we found all requested tags
|
|
||||||
if len(tags) != len(tagIDs) {
|
|
||||||
tx.Rollback()
|
|
||||||
log.Error("Not all tags were found. Requested: %d, Found: %d", len(tagIDs), len(tags))
|
|
||||||
http.Error(w, "One or more tags not found", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct SQL approach to insert associations
|
|
||||||
for _, tag := range tags {
|
|
||||||
// Check if association already exists using count
|
|
||||||
var count int64
|
|
||||||
err := tx.Table("item_tags").
|
|
||||||
Where("item_id = ? AND tag_id = ?", item.ID, tag.ID).
|
|
||||||
Count(&count).Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.Error("Failed to check existing association: %v", err)
|
|
||||||
http.Error(w, "Failed to check existing association", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
// Insert new association
|
|
||||||
err = tx.Exec("INSERT INTO item_tags (item_id, tag_id) VALUES (?, ?)",
|
|
||||||
item.ID, tag.ID).Error
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.Error("Failed to insert tag association: %v", err)
|
|
||||||
http.Error(w, "Failed to add tag association", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("Successfully added tag %d to item %s", tag.ID, itemID)
|
|
||||||
} else {
|
|
||||||
log.Info("Tag %d already associated with item %s", tag.ID, itemID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit transaction
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
log.Error("Failed to commit tag additions: %v", err)
|
|
||||||
http.Error(w, "Failed to add tags", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch updated item with tags
|
|
||||||
var updatedItem Item
|
|
||||||
if err := db.Preload("Tags").First(&updatedItem, itemID).Error; err != nil {
|
|
||||||
log.Error("Failed to fetch updated item: %v", err)
|
|
||||||
http.Error(w, "Failed to fetch updated item", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("update", fmt.Sprintf("Added tags %v to item %s", tagIDs, itemID))
|
|
||||||
json.NewEncoder(w).Encode(updatedItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveItemTagHandler removes a tag from an item
|
|
||||||
func RemoveItemTagHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
itemID := vars["id"]
|
|
||||||
tagID := vars["tagId"]
|
|
||||||
|
|
||||||
log.Info("Attempting to remove tag %s from item %s", tagID, itemID)
|
|
||||||
|
|
||||||
// Begin transaction
|
|
||||||
tx := db.Begin()
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Delete the association directly
|
|
||||||
if err := tx.Exec("DELETE FROM item_tags WHERE item_id = ? AND tag_id = ?",
|
|
||||||
itemID, tagID).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.Error("Failed to remove tag %s from item %s: %v", tagID, itemID, err)
|
|
||||||
http.Error(w, "Failed to remove tag", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit transaction
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
log.Error("Failed to commit tag removal: %v", err)
|
|
||||||
http.Error(w, "Failed to remove tag", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch updated item with remaining tags
|
|
||||||
var updatedItem Item
|
|
||||||
if err := db.Preload("Tags").First(&updatedItem, itemID).Error; err != nil {
|
|
||||||
log.Error("Failed to fetch updated item: %v", err)
|
|
||||||
http.Error(w, "Failed to fetch updated item", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DatabaseAction("update", fmt.Sprintf("Removed tag %s from item %s", tagID, itemID))
|
|
||||||
json.NewEncoder(w).Encode(updatedItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetItemsByTagHandler returns all items with a specific tag
|
|
||||||
func GetItemsByTagHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := GetLogger()
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
tagID := vars["id"]
|
|
||||||
|
|
||||||
var items []Item
|
|
||||||
if err := db.Joins("JOIN item_tags ON items.id = item_tags.item_id").
|
|
||||||
Where("item_tags.tag_id = ?", tagID).
|
|
||||||
Preload("Tags"). // Always preload tags for items
|
|
||||||
Find(&items).Error; err != nil {
|
|
||||||
log.Error("Failed to fetch items by tag: %v", err)
|
|
||||||
http.Error(w, "Failed to fetch items", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(items)
|
|
||||||
}
|
|
Loading…
Reference in New Issue