Compare commits

..

18 Commits
main ... api

Author SHA1 Message Date
Steve White 9a70d1130d Commented out some bs 2024-10-23 21:49:03 -05:00
Steve White 36ce2e9de8 Fixed static routing ... again ... I think 2024-10-23 21:47:58 -05:00
Steve White 7c34878c19 working on image archive 2024-10-23 17:56:10 -05:00
Steve White 44c615a120 Merge is necessary due to poor repository hygiene.
Merge branch 'api' of gitea.r8z.us:stwhite/boxes-api into api
2024-10-23 17:03:06 -05:00
Steve White c624ac4c67 setting uyp for image backup and restore 2024-10-23 17:02:19 -05:00
Steve White 96d2aeae19 Added docker-compose file to start service 2024-10-23 12:51:43 -05:00
Steve White 188f09e5fe Fixed so that go serves static REACT files as well 2024-10-23 12:47:16 -05:00
Steve White 998ae4295b changed api endpoints to include /api/v1 for proxy magic 2024-10-21 11:28:30 -05:00
Steve White 4b2b8e96c1 add /api/v1 to endpoint path for proxying. 2024-10-21 11:06:32 -05:00
Steve White 0f33941e65 nade CORS allowed origins a variable 2024-10-18 10:00:04 -05:00
Steve White ccb53c85c6 enabled sqlite3 auto_vacuum 2024-10-17 10:07:12 -05:00
Steve White 0f2b213406 Encrypt passwords in the database 2024-10-17 10:03:07 -05:00
Steve White 2689d2296c removed the "Getting Image" log message 2024-10-17 09:41:37 -05:00
Steve White 75c8dde932 added email to user database 2024-10-17 09:38:52 -05:00
Steve White 6660c1e3b3 user and database administration 2024-10-17 00:08:28 -05:00
Steve White 9bdcc1f7db added db restore 2024-10-16 17:28:00 -05:00
Steve White 2d63c02048 added admin functions for user management and working on database management 2024-10-16 17:10:20 -05:00
Steve White 3cb082b3b0 Updating readme 2024-10-15 17:37:16 -05:00
21 changed files with 616 additions and 27 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
data/* data/*
images/* images/*
.DS_Store .DS_Store
build/*

View File

@ -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. 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.
@ -13,6 +14,7 @@ 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
@ -21,10 +23,12 @@ 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`
@ -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. 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`
@ -60,11 +65,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
@ -72,11 +77,13 @@ The server also expects the CONFIG environment variable to be set. e.g. `export
### 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
``` ```
@ -84,17 +91,20 @@ The server also expects the CONFIG environment variable to be set. e.g. `export
### 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
``` ```
@ -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). - `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 ## Authentication API
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:
@ -117,9 +127,11 @@ 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.

189
admin.go Normal file
View File

@ -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
}

View File

@ -16,6 +16,8 @@ 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"`
StaticFilesDir string `yaml:"static_files_dir"`
AllowedOrigins string `yaml:"allowed_origins"`
} }
func LoadConfig(configPath string) (*Config, error) { func LoadConfig(configPath string) (*Config, error) {
@ -37,5 +39,13 @@ 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:3000"
}
return &config, nil return &config, nil
} }

View File

@ -1,6 +1,8 @@
database_path: "data/boxes.db" database_path: "/app/data/boxes.db"
test_database_path: "data/test_database.db" test_database_path: "/app/data/test_database.db"
jwt_secret: "super_secret_key" jwt_secret: "super_secret_key"
image_storage_dir: "images" image_storage_dir: "/app/images/"
listening_port: 8080 listening_port: 8080
log_file: "boxes.log" log_file: "boxes.log"
static_files_dir: "/app/build/"
allowed_origins: "*"

4
db.go
View File

@ -27,6 +27,7 @@ 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) {
@ -35,6 +36,9 @@ 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{}) db.AutoMigrate(&Box{}, &Item{}, &User{})

22
docker-compose.yml Normal file
View File

@ -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
View File

@ -7,14 +7,12 @@ 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 golang.org/x/crypto v0.28.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
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

13
go.sum
View File

@ -1,7 +1,5 @@
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=
@ -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/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 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=
@ -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/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=

View File

@ -1,17 +1,25 @@
package main package main
import ( import (
"archive/tar"
"compress/gzip"
"context" "context"
"crypto/md5"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/pkg/errors"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
) )
// Define contextKey globally within the package // Define contextKey globally within the package
@ -32,6 +40,7 @@ type LoginResponse struct {
} }
// loginHandler handles the /login endpoint. // loginHandler handles the /login endpoint.
// LoginHandler handles the /login endpoint.
func LoginHandler(w http.ResponseWriter, r *http.Request) { func LoginHandler(w http.ResponseWriter, r *http.Request) {
var req LoginRequest var req LoginRequest
fmt.Println(db, config) 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 // Check if the user exists and the password matches
var user User var user User
db.Where("username = ?", req.Username).First(&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) http.Error(w, "Invalid username or password", http.StatusUnauthorized)
return return
} }
@ -240,7 +256,7 @@ func UploadItemImageHandler(w http.ResponseWriter, r *http.Request) {
func GetItemImageHandler(w http.ResponseWriter, r *http.Request) { func GetItemImageHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
itemID := vars["id"] itemID := vars["id"]
fmt.Println("Getting image") // fmt.Println("Getting image")
// Retrieve the item from the database // Retrieve the item from the database
var item Item var item Item
if err := db.First(&item, itemID).Error; err != nil { if err := db.First(&item, itemID).Error; err != nil {
@ -402,3 +418,79 @@ func AuthMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r) 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
View File

@ -5,6 +5,8 @@ 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"
@ -18,10 +20,30 @@ var (
func main() { func main() {
configFile := os.Getenv("CONFIG") configFile := os.Getenv("BOXES_API_CONFIG")
var err error var err error
config, err = LoadConfig(configFile) 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 // check for errors
if err != nil || config == nil { if err != nil || config == nil {
log.Fatalf("Failed to load config: %v", err) log.Fatalf("Failed to load config: %v", err)
@ -40,10 +62,27 @@ func main() {
} }
defer db.Close() 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!") fmt.Println("Default user 'boxuser' created successfully!")
// Create the router // Create the router
router := mux.NewRouter() baseRouter := mux.NewRouter()
router := baseRouter.PathPrefix("/api/v1").Subrouter()
// Define your routes // Define your routes
router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS") router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS")
@ -65,15 +104,55 @@ func main() {
Methods("POST"). Methods("POST").
Handler(AuthMiddleware(http.HandlerFunc(UploadItemImageHandler))) 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 // Apply CORS middleware
c := cors.New(cors.Options{ 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"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Content-Type"}, 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, AllowCredentials: true,
}) })
// Start the server with CORS middleware // Start the server with CORS middleware
fmt.Printf("Server listening on port %d\n", config.ListeningPort) 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))
} }

View File

@ -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."

19
scripts/backup_db.bash Normal file
View File

@ -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

View File

@ -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');"

23
scripts/deleteuser.bash Normal file
View File

@ -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" \

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# API base URL # API base URL
API_BASE_URL="http://10.0.0.66:8080" API_BASE_URL="http://localhost:8080/api/v1"
# Login credentials # Login credentials
USERNAME="boxuser" USERNAME="boxuser"

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# API base URL # API base URL
API_BASE_URL="http://localhost:8080" API_BASE_URL="http://localhost:8080/api/v1"
# Login credentials # Login credentials
USERNAME="boxuser" USERNAME="boxuser"

18
scripts/getusers.bash Normal file
View File

@ -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
scripts/images.tar.gz Normal file
View File

24
scripts/makeuser.bash Normal file
View File

@ -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"

18
scripts/restore_db.bash Normal file
View File

@ -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"