refactored by AI to be clearer and easier to support. Seems to work.

This commit is contained in:
Steve White 2024-10-25 13:23:41 -05:00
parent 76a03cda57
commit 54b412a3fe
12 changed files with 233 additions and 118 deletions

1
.gitignore vendored
View File

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

View File

@ -71,7 +71,7 @@ func BackupDatabaseHandler(w http.ResponseWriter, r *http.Request) {
// ... // ...
fmt.Println("BackupDatabaseHandler called") fmt.Println("BackupDatabaseHandler called")
// Open the database file using the path from the config // Open the database file using the path from the config
file, err := os.Open(config.DatabasePath) file, err := os.Open(*DatabasePath)
if err != nil { if err != nil {
http.Error(w, "Failed to open database file", http.StatusInternalServerError) http.Error(w, "Failed to open database file", http.StatusInternalServerError)
return return
@ -122,13 +122,13 @@ func RestoreDatabaseHandler(w http.ResponseWriter, r *http.Request) {
func createDatabaseBackup() error { func createDatabaseBackup() error {
// Create a backup of the existing database // Create a backup of the existing database
src, err := os.Open(config.DatabasePath) src, err := os.Open(*DatabasePath)
if err != nil { if err != nil {
return err return err
} }
defer src.Close() defer src.Close()
dst, err := os.Create(config.DatabasePath + ".bak") dst, err := os.Create(*DatabasePath + ".bak")
if err != nil { if err != nil {
return err return err
} }
@ -146,7 +146,7 @@ func saveNewDatabase(r *http.Request) error {
} }
defer file.Close() defer file.Close()
dst, err := os.Create(config.DatabasePath) dst, err := os.Create(*DatabasePath)
if err != nil { if err != nil {
return err return err
} }
@ -158,7 +158,7 @@ func saveNewDatabase(r *http.Request) error {
func validateNewDatabase() error { func validateNewDatabase() error {
// Validate the new database is properly initialized // Validate the new database is properly initialized
db, err := ConnectDB(config.DatabasePath) db, err := ConnectDB(*DatabasePath)
if err != nil { if err != nil {
return err return err
} }
@ -179,7 +179,7 @@ func validateNewDatabase() error {
func switchToNewDatabase() error { func switchToNewDatabase() error {
// Switch to the new database app-wide // Switch to the new database app-wide
db, err := ConnectDB(config.DatabasePath) db, err := ConnectDB(*DatabasePath)
if err != nil { if err != nil {
return err return err
} }

View File

@ -44,7 +44,7 @@ func LoadConfig(configPath string) (*Config, error) {
} }
if config.AllowedOrigins == "" { if config.AllowedOrigins == "" {
config.AllowedOrigins = "http://localhost:3000" config.AllowedOrigins = "http://localhost:8080"
} }
return &config, nil return &config, nil

0
config/boxes.db Normal file
View File

View File

@ -1,7 +1,7 @@
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/" static_files_dir: "/app/build/"

View File

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

View File

@ -32,11 +32,32 @@ type LoginResponse struct {
Token string `json:"token"` Token string `json:"token"`
} }
func init() {
fmt.Printf("handlers.go init: config = %+v", config)
}
// loginHandler handles the /login endpoint. // 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 is ", db)
//fmt.Println("config is ", config)
if db == nil {
fmt.Println("DB is nil")
http.Error(w, "Database not initialized", http.StatusInternalServerError)
return
}
fmt.Println("DB is not nil")
//if config == nil {
//fmt.Println("Config is nil in LoginHandler")
//h//ttp.Error(w, "Configuration not loaded", http.StatusInternalServerError)
//return
//}
//fmt.Println("Config is not nil")
fmt.Printf("DB: %+v\n", db)
//fmt.Printf("Config: %+v\n", config)
fmt.Println("LoginHandler called") fmt.Println("LoginHandler called")
err := json.NewDecoder(r.Body).Decode(&req) err := json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
@ -65,7 +86,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
"exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours "exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours
}) })
tokenString, err := token.SignedString([]byte(config.JWTSecret)) tokenString, err := token.SignedString(*JWTSecret)
if err != nil { if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError) http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return return
@ -213,12 +234,12 @@ func UploadItemImageHandler(w http.ResponseWriter, r *http.Request) {
// Save the uploaded file locally or to a storage service // Save the uploaded file locally or to a storage service
// Ensure the directory exists // Ensure the directory exists
if err := os.MkdirAll(config.ImageStorageDir, 0755); err != nil { if err := os.MkdirAll(*ImageStorage, 0755); err != nil {
http.Error(w, "Unable to create image storage directory", http.StatusInternalServerError) http.Error(w, "Unable to create image storage directory", http.StatusInternalServerError)
return return
} }
filePath := fmt.Sprintf("%s/%s", config.ImageStorageDir, handler.Filename) filePath := fmt.Sprintf("%s/%s", *ImageStorage, handler.Filename)
outFile, err := os.Create(filePath) outFile, err := os.Create(filePath)
if err != nil { if err != nil {
http.Error(w, "Unable to save the file", http.StatusInternalServerError) http.Error(w, "Unable to save the file", http.StatusInternalServerError)
@ -390,7 +411,7 @@ func AuthMiddleware(next http.Handler) http.Handler {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
return []byte(config.JWTSecret), nil return *JWTSecret, nil
}) })
if err != nil || !token.Valid { if err != nil || !token.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized) http.Error(w, "Invalid token", http.StatusUnauthorized)

240
main.go
View File

@ -5,8 +5,8 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"strings"
"path/filepath" "path/filepath"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
@ -14,56 +14,131 @@ import (
) )
var ( var (
db *gorm.DB // Declare db globally db *gorm.DB // Declare db globally
config *Config config *Config
JWTSecret *[]byte
ImageStorage *string
DatabasePath *string
) )
func main() { func main() {
// Load configuration
configFile := os.Getenv("BOXES_API_CONFIG") config, err := loadAndValidateConfig()
var err error if err != nil {
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) log.Fatalf("Failed to load config: %v", err)
} }
fmt.Printf("Config loaded successfully in main(), DB path %s", config.DatabasePath)
fmt.Println(config.DatabasePath)
fmt.Println(config.ImageStorageDir)
fmt.Println(config.JWTSecret)
fmt.Println(config.LogFile)
fmt.Println(config.ListeningPort)
// Connect to the database // Connect to the database
db, err = ConnectDB(config.DatabasePath) db, err = connectToDatabase(config)
if err != nil || db == nil { if err != nil {
log.Fatalf("Failed to connect to database: %v", err) log.Fatalf("Failed to connect to database: %v", err)
} }
defer db.Close() defer db.Close()
// Modify your custom handler to include more detailed logging // Create routers
customHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { baseRouter := mux.NewRouter()
apiRouter := createAPIRouter(baseRouter)
staticRouter := createStaticRouter(baseRouter, config.StaticFilesDir)
// Set up routes
setupAPIRoutes(apiRouter)
setupStaticRoutes(staticRouter, config.StaticFilesDir)
// Create CORS handler
corsHandler := createCORSHandler(config.AllowedOrigins)
// Start the server
startServer(config.ListeningPort, corsHandler(baseRouter))
}
func loadAndValidateConfig() (*Config, error) {
configFile := os.Getenv("BOXES_API_CONFIG")
config, err := LoadConfig(configFile)
if err != nil || config == nil {
return nil, fmt.Errorf("failed to load config: %v", err)
}
// Validate the database path
if config.DatabasePath == "" {
return nil, fmt.Errorf("database path is not set in config")
}
DatabasePath = &config.DatabasePath
// Set JWTSecret
jwtSecretBytes := []byte(config.JWTSecret)
JWTSecret = &jwtSecretBytes
ImageStorage = &config.ImageStorageDir
// Log config details
logConfigDetails(config)
return config, nil
}
func connectToDatabase(config *Config) (*gorm.DB, error) {
if config == nil {
return nil, fmt.Errorf("config is nil in connectToDatabase")
}
db, err := ConnectDB(config.DatabasePath)
if err != nil || db == nil {
return nil, fmt.Errorf("failed to connect to database: %v", err)
}
fmt.Println("Connected to database in connectToDatabase")
return db, nil
}
func createAPIRouter(baseRouter *mux.Router) *mux.Router {
return baseRouter.PathPrefix("/api/v1").Subrouter()
}
func createStaticRouter(baseRouter *mux.Router, staticPath string) *mux.Router {
if err := validateStaticDirectory(staticPath); err != nil {
log.Fatalf("Static directory error: %v", err)
}
return baseRouter.PathPrefix("/").Subrouter()
}
func setupAPIRoutes(router *mux.Router) {
router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS")
// Protected routes
protected := router.NewRoute().Subrouter()
protected.Use(AuthMiddleware)
protected.Handle("/boxes", http.HandlerFunc(GetBoxesHandler)).Methods("GET", "OPTIONS")
protected.Handle("/boxes", http.HandlerFunc(CreateBoxHandler)).Methods("POST", "OPTIONS")
protected.Handle("/boxes/{id}", http.HandlerFunc(DeleteBoxHandler)).Methods("DELETE", "OPTIONS")
protected.Handle("/boxes/{id}", http.HandlerFunc(GetBoxHandler)).Methods("GET", "OPTIONS")
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("/boxes/{id}/items", http.HandlerFunc(GetItemsInBoxHandler)).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("/search/items", http.HandlerFunc(SearchItemsHandler)).Methods("GET", "OPTIONS")
protected.Handle("/items/{id}/upload", http.HandlerFunc(UploadItemImageHandler)).Methods("POST", "OPTIONS")
setupManagementRoutes(protected.PathPrefix("/admin").Subrouter())
}
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) {
customHandler := createCustomStaticHandler(staticPath)
router.PathPrefix("/").Handler(http.StripPrefix("/", customHandler))
}
func createCustomStaticHandler(staticPath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("Attempting to serve: %s from directory: %s", r.URL.Path, staticPath) log.Printf("Attempting to serve: %s from directory: %s", r.URL.Path, staticPath)
fullPath := filepath.Join(staticPath, r.URL.Path) fullPath := filepath.Join(staticPath, r.URL.Path)
if _, err := os.Stat(fullPath); os.IsNotExist(err) { if _, err := os.Stat(fullPath); os.IsNotExist(err) {
@ -72,65 +147,42 @@ func main() {
return return
} }
http.FileServer(http.Dir(staticPath)).ServeHTTP(w, r) http.FileServer(http.Dir(staticPath)).ServeHTTP(w, r)
}) }
}
fmt.Println("Default user 'boxuser' created successfully!") func createCORSHandler(allowedOrigins string) func(http.Handler) http.Handler {
origins := strings.Split(allowedOrigins, ",")
if len(origins) == 0 {
origins = []string{"http://localhost:3000"}
}
// Create the router return cors.New(cors.Options{
baseRouter := mux.NewRouter()
router := baseRouter.PathPrefix("/api/v1").Subrouter()
staticRouter := baseRouter.PathPrefix("/").Subrouter()
// Define your routes
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)))
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")
// 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))
// Apply CORS middleware
c := cors.New(cors.Options{
AllowedOrigins: origins, 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"}, 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,
}) }).Handler
}
// Start the server with CORS middleware
fmt.Printf("Server listening on port %d\n", config.ListeningPort) func startServer(port int, handler http.Handler) {
http.ListenAndServe(fmt.Sprintf(":%d", config.ListeningPort), c.Handler(baseRouter)) fmt.Printf("Server listening on port %d\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), handler))
}
func validateStaticDirectory(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("static directory does not exist: %s", path)
}
return nil
}
func logConfigDetails(config *Config) {
fmt.Println("Config details:")
fmt.Printf("Database Path: %s\n", config.DatabasePath)
fmt.Printf("Image Storage Dir: %s\n", config.ImageStorageDir)
fmt.Printf("JWT Secret: %s\n", config.JWTSecret)
fmt.Printf("Log File: %s\n", config.LogFile)
fmt.Printf("Listening Port: %d\n", config.ListeningPort)
fmt.Printf("Allowed Origins: %s\n", config.AllowedOrigins)
} }

View File

@ -1,10 +1,19 @@
#!/binb/ash #!/binb/ash
# Set the password you want to use # Set the password you want to use
PASSWORD="12m0nk3ys" 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 # Use htpasswd to create a bcrypt hash of the password
HASHED_PASSWORD=$(htpasswd -nbBC 10 "" "$PASSWORD" | tr -d ':\n') HASHED_PASSWORD=$(htpasswd -nbBC 10 "" "$PASSWORD" | tr -d ':\n')
# Create the user in the SQLite database # 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');" <<<<<<< 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

31
scripts/get_token.bash Normal file
View File

@ -0,0 +1,31 @@
#!/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

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"
@ -11,6 +11,7 @@ PASSWORD="boxuser"
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \ TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \ -d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \
"$API_BASE_URL/login" | jq -r '.token') "$API_BASE_URL/login" | jq -r '.token')
echo $TOKEN
curl -X GET \ curl -X GET \
$API_BASE_URL/admin/user \ $API_BASE_URL/admin/user \