Compare commits

..

1 Commits
admin ... main

Author SHA1 Message Date
Steve White 63dce0b348 Update README.md 2024-10-15 22:21:52 +00:00
13 changed files with 19 additions and 358 deletions

View File

@ -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.
## Features
- Manage boxes and items stored within them.
- JWT-based authentication.
- 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.
## Tech Stack
- **Language:** Go
- **Database:** SQLite3
- **Authentication:** JWT
@ -23,12 +21,10 @@ 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`
@ -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.
### Items
- **GET** `/items`
Retrieves all items, optionally searchable by description.
- **POST** `/items`
@ -65,11 +60,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
@ -77,13 +72,11 @@ image_storage_directory: "./images"
### 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
```
@ -91,20 +84,17 @@ image_storage_directory: "./images"
### 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
```
@ -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).
- `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:
@ -127,11 +117,9 @@ 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.

189
admin.go
View File

@ -1,189 +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) {
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
}

4
db.go
View File

@ -27,7 +27,6 @@ type User struct {
gorm.Model
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
}
func ConnectDB(dbPath string) (*gorm.DB, error) {
@ -36,9 +35,6 @@ 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{})

5
go.mod
View File

@ -7,11 +7,14 @@ 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
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
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
View File

@ -1,5 +1,7 @@
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=
@ -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/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/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=
@ -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/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=

View File

@ -12,7 +12,6 @@ import (
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
)
// Define contextKey globally within the package
@ -33,7 +32,6 @@ 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)
@ -47,14 +45,7 @@ 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 {
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 user.ID == 0 || user.Password != req.Password {
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
return
}
@ -249,7 +240,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 {

27
main.go
View File

@ -5,7 +5,6 @@ import (
"log"
"net/http"
"os"
"strings"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
@ -19,21 +18,10 @@ var (
func main() {
configFile := os.Getenv("BOXES_API_CONFIG")
configFile := os.Getenv("CONFIG")
var err error
config, err = LoadConfig(configFile)
// Get the allowed origins from the ALLOWED_ORIGINS environment variable
// If empty, defaults to http://localhost:3000
allowedOrigins := os.Getenv("BOXES_API_ALLOWED_ORIGINS")
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)
@ -77,22 +65,11 @@ 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")
// Apply CORS middleware
c := cors.New(cors.Options{
AllowedOrigins: origins,
AllowedOrigins: []string{"http://localhost:3000", "http://10.0.0.16:3000"}, // Change this to your frontend domain
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,
})

View File

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

View File

@ -1,10 +0,0 @@
#!/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');"

View File

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

View File

@ -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 GET \
$API_BASE_URL/admin/user \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"

View File

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

View File

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