Compare commits
18 Commits
Author | SHA1 | Date |
---|---|---|
Steve White | 9a70d1130d | |
Steve White | 36ce2e9de8 | |
Steve White | 7c34878c19 | |
Steve White | 44c615a120 | |
Steve White | c624ac4c67 | |
Steve White | 96d2aeae19 | |
Steve White | 188f09e5fe | |
Steve White | 998ae4295b | |
Steve White | 4b2b8e96c1 | |
Steve White | 0f33941e65 | |
Steve White | ccb53c85c6 | |
Steve White | 0f2b213406 | |
Steve White | 2689d2296c | |
Steve White | 75c8dde932 | |
Steve White | 6660c1e3b3 | |
Steve White | 9bdcc1f7db | |
Steve White | 2d63c02048 | |
Steve White | 3cb082b3b0 |
|
@ -2,3 +2,4 @@
|
|||
data/*
|
||||
images/*
|
||||
.DS_Store
|
||||
build/*
|
||||
|
|
16
README.md
16
README.md
|
@ -4,6 +4,7 @@
|
|||
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
|
||||
|
||||
- Manage boxes and items stored within them.
|
||||
- JWT-based authentication.
|
||||
- Automatic database creation if it doesn't exist.
|
||||
|
@ -13,6 +14,7 @@ This project is a backend API built using Go, designed for managing boxes and th
|
|||
- Containerized using Docker.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Language:** Go
|
||||
- **Database:** SQLite3
|
||||
- **Authentication:** JWT
|
||||
|
@ -21,10 +23,12 @@ This project is a backend API built using Go, designed for managing boxes and th
|
|||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
|
||||
- **POST** `/login`
|
||||
Authenticates a user and returns a JWT.
|
||||
|
||||
### Boxes
|
||||
|
||||
- **GET** `/boxes`
|
||||
Retrieves all boxes.
|
||||
- **POST** `/boxes`
|
||||
|
@ -35,6 +39,7 @@ This project is a backend API built using Go, designed for managing boxes and th
|
|||
Retrieves all items in a box by its ID.
|
||||
|
||||
### Items
|
||||
|
||||
- **GET** `/items`
|
||||
Retrieves all items, optionally searchable by description.
|
||||
- **POST** `/items`
|
||||
|
@ -60,11 +65,11 @@ jwt_secret: "your_jwt_secret"
|
|||
log_file: "./logs/app.log"
|
||||
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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker
|
||||
- Go (if running locally)
|
||||
- SQLite3
|
||||
|
@ -72,11 +77,13 @@ The server also expects the CONFIG environment variable to be set. e.g. `export
|
|||
### Running with Docker
|
||||
|
||||
1. Build the Docker image:
|
||||
|
||||
```bash
|
||||
docker build -t box-management-api .
|
||||
```
|
||||
|
||||
2. Run the Docker container:
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 box-management-api
|
||||
```
|
||||
|
@ -84,17 +91,20 @@ The server also expects the CONFIG environment variable to be set. e.g. `export
|
|||
### Running Locally
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-repo/box-management-api.git
|
||||
cd box-management-api
|
||||
```
|
||||
|
||||
2. Build the Go application:
|
||||
|
||||
```bash
|
||||
go build -o main .
|
||||
```
|
||||
|
||||
3. Run the application:
|
||||
|
||||
```bash
|
||||
./main
|
||||
```
|
||||
|
@ -107,7 +117,7 @@ The application creates the following SQLite3 tables:
|
|||
- `items`: Contains `id` (int), `name` (text), `description` (text), `box_id` (int), and `image_path` (text).
|
||||
- `users`: Contains `id` (int), `username` (text), and `password` (hashed).
|
||||
|
||||
## Authentication
|
||||
## Authentication API
|
||||
|
||||
The API uses JWT-based authentication. A default user is created on startup:
|
||||
|
||||
|
@ -117,9 +127,11 @@ The API uses JWT-based authentication. A default user is created on startup:
|
|||
## Logs
|
||||
|
||||
The following events are logged to the file specified in the configuration:
|
||||
|
||||
- User logins
|
||||
- Box creation/deletion
|
||||
- Item creation/deletion
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
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) {
|
||||
var users []User
|
||||
db.Find(&users)
|
||||
json.NewEncoder(w).Encode(users)
|
||||
}
|
||||
|
||||
// CreateUserHandler handles POST requests to /admin/user
|
||||
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var user User
|
||||
err := json.NewDecoder(r.Body).Decode(&user)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Hash the password before storing
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to hash password", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.Password = string(hashedPassword)
|
||||
|
||||
db.Create(&user)
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
// GetUserHandler handles GET requests to /admin/user/{id}
|
||||
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
var user User
|
||||
db.First(&user, id)
|
||||
if user.ID == 0 {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
// DeleteUserHandler handles DELETE requests to /admin/user/{id}
|
||||
func DeleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
var user User
|
||||
db.First(&user, id)
|
||||
if user.ID == 0 {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
db.Delete(&user)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// BackupDatabaseHandler handles GET requests to /admin/db
|
||||
func BackupDatabaseHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// ...
|
||||
fmt.Println("BackupDatabaseHandler called")
|
||||
// Open the database file using the path from the config
|
||||
file, err := os.Open(config.DatabasePath)
|
||||
if err != nil {
|
||||
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 {
|
||||
http.Error(w, "Failed to send database file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RestoreDatabaseHandler handles POST requests to /admin/db
|
||||
func RestoreDatabaseHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Create a backup of the existing database
|
||||
err := createDatabaseBackup()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create database backup", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Save the new database
|
||||
err = saveNewDatabase(r)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save new database", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the new database is properly initialized
|
||||
err = validateNewDatabase()
|
||||
if err != nil {
|
||||
http.Error(w, "New database is not properly initialized", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Switch to the new database app-wide
|
||||
err = switchToNewDatabase()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to switch to new database", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Println("Database restored successfully")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Database restored successfully"})
|
||||
}
|
||||
|
||||
func createDatabaseBackup() error {
|
||||
// Create a backup of the existing database
|
||||
src, err := os.Open(config.DatabasePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(config.DatabasePath + ".bak")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
return err
|
||||
}
|
||||
|
||||
func saveNewDatabase(r *http.Request) error {
|
||||
// Save the new database
|
||||
file, _, err := r.FormFile("database")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
dst, err := os.Create(config.DatabasePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
_, err = io.Copy(dst, file)
|
||||
return err
|
||||
}
|
||||
|
||||
func validateNewDatabase() error {
|
||||
// Validate the new database is properly initialized
|
||||
db, err := ConnectDB(config.DatabasePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Check if required tables exist
|
||||
tables := []string{"users", "boxes", "items"}
|
||||
for _, table := range tables {
|
||||
var count int
|
||||
db.Debug().Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?;", table).Row().Scan(&count)
|
||||
if count == 0 {
|
||||
return fmt.Errorf("table %s does not exist", table)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func switchToNewDatabase() error {
|
||||
// Switch to the new database app-wide
|
||||
db, err := ConnectDB(config.DatabasePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Update the db variable with the new database connection
|
||||
db = db
|
||||
return nil
|
||||
}
|
10
config.go
10
config.go
|
@ -16,6 +16,8 @@ type Config struct {
|
|||
ImageStorageDir string `yaml:"image_storage_dir"`
|
||||
ListeningPort int `yaml:"listening_port"`
|
||||
LogFile string `yaml:"log_file"`
|
||||
StaticFilesDir string `yaml:"static_files_dir"`
|
||||
AllowedOrigins string `yaml:"allowed_origins"`
|
||||
}
|
||||
|
||||
func LoadConfig(configPath string) (*Config, error) {
|
||||
|
@ -37,5 +39,13 @@ func LoadConfig(configPath string) (*Config, error) {
|
|||
config.DatabasePath = dbPath
|
||||
}
|
||||
|
||||
if config.StaticFilesDir == "" {
|
||||
config.StaticFilesDir = "build"
|
||||
}
|
||||
|
||||
if config.AllowedOrigins == "" {
|
||||
config.AllowedOrigins = "http://localhost:3000"
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
database_path: "data/boxes.db"
|
||||
test_database_path: "data/test_database.db"
|
||||
database_path: "/app/data/boxes.db"
|
||||
test_database_path: "/app/data/test_database.db"
|
||||
jwt_secret: "super_secret_key"
|
||||
image_storage_dir: "images"
|
||||
image_storage_dir: "/app/images/"
|
||||
listening_port: 8080
|
||||
log_file: "boxes.log"
|
||||
static_files_dir: "/app/build/"
|
||||
allowed_origins: "*"
|
||||
|
|
4
db.go
4
db.go
|
@ -27,6 +27,7 @@ type User struct {
|
|||
gorm.Model
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func ConnectDB(dbPath string) (*gorm.DB, error) {
|
||||
|
@ -35,6 +36,9 @@ func ConnectDB(dbPath string) (*gorm.DB, error) {
|
|||
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
|
||||
db.AutoMigrate(&Box{}, &Item{}, &User{})
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
version: '3.3' # Specify the Docker Compose file version
|
||||
|
||||
services:
|
||||
boxes-api:
|
||||
image: 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:
|
||||
- ./data:/app/data # Mount host data directory
|
||||
- ./images:/app/images # Mount host images directory
|
||||
- ./config:/app/config # Mount host config directory
|
||||
- ./build:/app/build
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
6
go.mod
6
go.mod
|
@ -7,14 +7,12 @@ require (
|
|||
github.com/gorilla/mux v1.8.1
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
)
|
||||
|
|
13
go.sum
13
go.sum
|
@ -1,7 +1,5 @@
|
|||
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/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/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
|
@ -24,16 +22,15 @@ 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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
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-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.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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
@ -46,5 +43,3 @@ 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/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
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=
|
||||
|
|
96
handlers.go
96
handlers.go
|
@ -1,17 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Define contextKey globally within the package
|
||||
|
@ -32,6 +40,7 @@ type LoginResponse struct {
|
|||
}
|
||||
|
||||
// loginHandler handles the /login endpoint.
|
||||
// LoginHandler handles the /login endpoint.
|
||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req LoginRequest
|
||||
fmt.Println(db, config)
|
||||
|
@ -45,7 +54,14 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||
// 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 {
|
||||
if user.ID == 0 {
|
||||
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Compare the provided password with the stored hashed password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
@ -240,7 +256,7 @@ func UploadItemImageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
func GetItemImageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
itemID := vars["id"]
|
||||
fmt.Println("Getting image")
|
||||
// fmt.Println("Getting image")
|
||||
// Retrieve the item from the database
|
||||
var item Item
|
||||
if err := db.First(&item, itemID).Error; err != nil {
|
||||
|
@ -402,3 +418,79 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
|||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func GetImageArchiveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fmt.Println("Getting image archive")
|
||||
// Create a pipe to write the archive to
|
||||
_, pw := io.Pipe()
|
||||
|
||||
// Create an MD5 hash writer
|
||||
hash := md5.New()
|
||||
|
||||
// Use a MultiWriter to write to both the pipe and the hash
|
||||
multiWriter := io.MultiWriter(pw, hash)
|
||||
|
||||
// Start a goroutine to create the archive
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
|
||||
gzipWriter := gzip.NewWriter(multiWriter)
|
||||
defer gzipWriter.Close()
|
||||
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
err := filepath.Walk(config.ImageStorageDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Failed to access path: %s", path))
|
||||
}
|
||||
|
||||
header, err := tar.FileInfoHeader(info, info.Name())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Failed to create tar header for: %s", path))
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(config.ImageStorageDir, path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Failed to get relative path for: %s", path))
|
||||
}
|
||||
header.Name = relPath
|
||||
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Failed to write tar header for: %s", path))
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Failed to open file: %s", path))
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(tarWriter, file)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Failed to copy file contents for: %s", path))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Set headers
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=images.tar.gz")
|
||||
|
||||
// Copy the archive to the response writer
|
||||
if err != nil {
|
||||
fmt.Println("Error in GetImageArchiveHandler:", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate and set the MD5 sum header
|
||||
md5sum := hex.EncodeToString(hash.Sum(nil))
|
||||
w.Header().Set("X-Archive-MD5", md5sum)
|
||||
|
||||
}()
|
||||
}
|
||||
|
|
87
main.go
87
main.go
|
@ -5,6 +5,8 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
@ -18,10 +20,30 @@ var (
|
|||
|
||||
func main() {
|
||||
|
||||
configFile := os.Getenv("CONFIG")
|
||||
configFile := os.Getenv("BOXES_API_CONFIG")
|
||||
var err error
|
||||
config, err = LoadConfig(configFile)
|
||||
|
||||
// get the static files directory
|
||||
staticPath := config.StaticFilesDir
|
||||
|
||||
// Add this before setting up the handler
|
||||
if _, err := os.Stat(staticPath); os.IsNotExist(err) {
|
||||
log.Fatalf("Static directory does not exist: %s", staticPath)
|
||||
}
|
||||
|
||||
// Get the allowed origins from the ALLOWED_ORIGINS environment variable
|
||||
// If empty, defaults to http://localhost:3000
|
||||
allowedOrigins := config.AllowedOrigins
|
||||
fmt.Println("Allowed origins: ", allowedOrigins)
|
||||
origins := []string{"http://localhost:3000"} // Default value
|
||||
|
||||
if allowedOrigins != "" {
|
||||
// Split the comma-separated string into a slice of strings
|
||||
origins = strings.Split(allowedOrigins, ",")
|
||||
fmt.Println("Listening for connections from: ", origins)
|
||||
}
|
||||
|
||||
// check for errors
|
||||
if err != nil || config == nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
|
@ -40,10 +62,27 @@ func main() {
|
|||
}
|
||||
defer db.Close()
|
||||
|
||||
// customHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// // Don't handle /static/ paths here - let the static file server handle those
|
||||
// if strings.HasPrefix(r.URL.Path, "/static/") {
|
||||
// http.NotFound(w, r)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // For all other routes, serve index.html
|
||||
// indexPath := filepath.Join(staticPath, "index.html")
|
||||
// http.ServeFile(w, r, indexPath)
|
||||
// })
|
||||
|
||||
// Register the catch-all handler for non-static routes
|
||||
//staticRouter.PathPrefix("/").Handler(customHandler)
|
||||
|
||||
fmt.Println("Default user 'boxuser' created successfully!")
|
||||
|
||||
// Create the router
|
||||
router := mux.NewRouter()
|
||||
baseRouter := mux.NewRouter()
|
||||
|
||||
router := baseRouter.PathPrefix("/api/v1").Subrouter()
|
||||
|
||||
// Define your routes
|
||||
router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS")
|
||||
|
@ -65,15 +104,55 @@ func main() {
|
|||
Methods("POST").
|
||||
Handler(AuthMiddleware(http.HandlerFunc(UploadItemImageHandler)))
|
||||
|
||||
managementRouter := router.PathPrefix("/admin").Subrouter()
|
||||
managementRouter.Use(AuthMiddleware)
|
||||
|
||||
managementRouter.Handle("/user", http.HandlerFunc(GetUsersHandler)).Methods("GET", "OPTIONS")
|
||||
managementRouter.Handle("/user", http.HandlerFunc(CreateUserHandler)).Methods("POST", "OPTIONS")
|
||||
managementRouter.Handle("/user/{id}", http.HandlerFunc(GetUserHandler)).Methods("GET", "OPTIONS")
|
||||
managementRouter.Handle("/user/{id}", http.HandlerFunc(DeleteUserHandler)).Methods("DELETE", "OPTIONS")
|
||||
managementRouter.Handle("/db", http.HandlerFunc(BackupDatabaseHandler)).Methods("GET", "OPTIONS")
|
||||
managementRouter.Handle("/db", http.HandlerFunc(RestoreDatabaseHandler)).Methods("POST", "OPTIONS")
|
||||
managementRouter.Handle("/imagearchive", http.HandlerFunc(GetImageArchiveHandler)).Methods("GET", "OPTIONS")
|
||||
|
||||
// Define a route for serving static files
|
||||
fmt.Println("Serving static files from:", staticPath)
|
||||
//staticHandler := http.FileServer(http.Dir(staticPath))
|
||||
fmt.Println("Registering route for serving static files, no StripPrefix")
|
||||
//staticRouter.HandleFunc("/", customHandler).Methods("GET", "OPTIONS")
|
||||
// perplexity recommends:
|
||||
//staticRouter.PathPrefix("/").Handler(http.StripPrefix("/", customHandler))
|
||||
// Replace your existing static route with this
|
||||
|
||||
// Create a dedicated file server for static files
|
||||
fileServer := http.FileServer(http.Dir(staticPath))
|
||||
|
||||
// Register the static file handler with explicit path stripping
|
||||
//staticRouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", staticFS))
|
||||
baseRouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
|
||||
|
||||
baseRouter.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Don't handle /static/ paths here
|
||||
if strings.HasPrefix(r.URL.Path, "/static/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// For all other routes, serve index.html
|
||||
indexPath := filepath.Join(staticPath, "index.html")
|
||||
http.ServeFile(w, r, indexPath)
|
||||
}))
|
||||
|
||||
// Apply CORS middleware
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"http://localhost:3000", "http://10.0.0.16:3000"}, // Change this to your frontend domain
|
||||
AllowedOrigins: origins,
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Authorization", "Content-Type"},
|
||||
ExposedHeaders: []string{"Content-Length", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma", "ETag"},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
|
||||
// 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))
|
||||
http.ListenAndServe(fmt.Sprintf(":%d", config.ListeningPort), c.Handler(baseRouter))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Set variables
|
||||
SERVER_URL="http://localhost:8080" # Replace with your server URL
|
||||
ENDPOINT="/api/v1/admin/imagearchive"
|
||||
OUTPUT_FILE="images.tar.gz"
|
||||
EXTRACT_DIR="extracted_images"
|
||||
USERNAME=boxuser
|
||||
PASSWORD=boxuser
|
||||
|
||||
AUTH_TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
|
||||
"$SERVER_URL/api/v1/login" | jq -r '.token')
|
||||
|
||||
echo $AUTH_TOKEN
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists () {
|
||||
type "$1" &> /dev/null ;
|
||||
}
|
||||
|
||||
# Check if required commands exist
|
||||
if ! command_exists curl || ! command_exists md5sum || ! command_exists tar; then
|
||||
echo "Error: This script requires curl, md5sum, and tar. Please install them and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download the archive
|
||||
echo "Downloading archive..."
|
||||
HTTP_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -o "$OUTPUT_FILE" \
|
||||
-H "Authorization: Bearer $AUTH_TOKEN" \
|
||||
"$SERVER_URL/api/v1/admin/imagearchive" )
|
||||
HTTP_BODY=$(echo $HTTP_RESPONSE | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
HTTP_STATUS=$(echo $HTTP_RESPONSE | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ $HTTP_STATUS -ne 200 ]; then
|
||||
echo "Error: HTTP Status $HTTP_STATUS"
|
||||
echo "Response: $HTTP_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get MD5 sum from header
|
||||
HEADER_MD5=$(curl -sI -H "Authorization: Bearer $AUTH_TOKEN" "$SERVER_URL$ENDPOINT" | grep X-Archive-MD5 | cut -d' ' -f2 | tr -d '\r')
|
||||
|
||||
# Calculate MD5 sum of downloaded file
|
||||
CALCULATED_MD5=$(md5sum "$OUTPUT_FILE" | cut -d' ' -f1)
|
||||
|
||||
echo "MD5 from header: $HEADER_MD5"
|
||||
echo "Calculated MD5: $CALCULATED_MD5"
|
||||
|
||||
# Compare MD5 sums
|
||||
if [ "$HEADER_MD5" = "$CALCULATED_MD5" ]; then
|
||||
echo "MD5 sums match. File integrity verified."
|
||||
else
|
||||
echo "MD5 sums do not match. File may be corrupted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the archive
|
||||
echo "Extracting archive..."
|
||||
mkdir -p "$EXTRACT_DIR"
|
||||
tar -xzvf "$OUTPUT_FILE" -C "$EXTRACT_DIR"
|
||||
|
||||
# List contents of extracted directory
|
||||
echo "Contents of extracted archive:"
|
||||
find "$EXTRACT_DIR" -type f | sed "s|$EXTRACT_DIR/||"
|
||||
|
||||
# Clean up
|
||||
echo "Cleaning up..."
|
||||
rm "$OUTPUT_FILE"
|
||||
rm -r "$EXTRACT_DIR"
|
||||
|
||||
echo "Test completed successfully."
|
|
@ -0,0 +1,19 @@
|
|||
#!/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
|
|
@ -0,0 +1,10 @@
|
|||
#!/binb/ash
|
||||
|
||||
# Set the password you want to use
|
||||
PASSWORD="12m0nk3ys"
|
||||
|
||||
# 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
|
||||
sqlite3 your_database.db "INSERT INTO users (username, password, email, email, email, email, email, email, email, email, email) VALUES ('boxuser', '$HASHED_PASSWORD','boxuser@r8z.us');"
|
|
@ -0,0 +1,23 @@
|
|||
#!/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,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
# API base URL
|
||||
API_BASE_URL="http://10.0.0.66:8080"
|
||||
API_BASE_URL="http://localhost:8080/api/v1"
|
||||
|
||||
# Login credentials
|
||||
USERNAME="boxuser"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
# API base URL
|
||||
API_BASE_URL="http://localhost:8080"
|
||||
API_BASE_URL="http://localhost:8080/api/v1"
|
||||
|
||||
# Login credentials
|
||||
USERNAME="boxuser"
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
#!/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')
|
||||
|
||||
curl -X GET \
|
||||
$API_BASE_URL/admin/user \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json"
|
|
@ -0,0 +1,24 @@
|
|||
#!/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"
|
|
@ -0,0 +1,18 @@
|
|||
#!/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"
|
Loading…
Reference in New Issue