Compare commits
10 commits
e7c2946d32
...
e3d8f9e8ee
Author | SHA1 | Date | |
---|---|---|---|
e3d8f9e8ee | |||
7cbe2a07d3 | |||
29ec555090 | |||
005b04e380 | |||
b5fe07d07e | |||
46cecdc007 | |||
0299e2891c | |||
540c69d7d6 | |||
364a27e44f | |||
e3d07fb1ca |
8 changed files with 805 additions and 58 deletions
31
api/api.go
Normal file
31
api/api.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
db "github.com/your-username/your-repo/database/sqlite/generated"
|
||||||
|
)
|
||||||
|
|
||||||
|
// API holds all API handlers
|
||||||
|
type API struct {
|
||||||
|
Storage *StorageHandler
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new API instance
|
||||||
|
func New(dbQueries *db.Queries, logger *zap.Logger) *API {
|
||||||
|
return &API{
|
||||||
|
Storage: NewStorageHandler(dbQueries, logger),
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes registers all API routes
|
||||||
|
func (a *API) RegisterRoutes(e *echo.Echo, middleware echo.MiddlewareFunc) {
|
||||||
|
// Create API group with middleware
|
||||||
|
api := e.Group("/api/v1", middleware)
|
||||||
|
|
||||||
|
// Register routes for each handler
|
||||||
|
a.Storage.RegisterRoutes(api)
|
||||||
|
}
|
193
api/storage.go
Normal file
193
api/storage.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
db "github.com/your-username/your-repo/database/sqlite/generated"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StorageHandler struct {
|
||||||
|
db *db.Queries
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorageHandler(db *db.Queries, logger *zap.Logger) *StorageHandler {
|
||||||
|
return &StorageHandler{
|
||||||
|
db: db,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStorageSpaces returns all storage spaces
|
||||||
|
func (h *StorageHandler) GetStorageSpaces(c echo.Context) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
storageSpaces, err := h.db.GetAllStorageSpaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("Failed to fetch storage spaces", zap.Error(err))
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
||||||
|
"error": "Failed to fetch storage spaces",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to a format suitable for the frontend
|
||||||
|
result := make([]map[string]interface{}, len(storageSpaces))
|
||||||
|
for i, space := range storageSpaces {
|
||||||
|
result[i] = map[string]interface{}{
|
||||||
|
"id": space.ID,
|
||||||
|
"parent": space.Parent,
|
||||||
|
"location": space.Location,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectsInStorage returns all objects in a specific storage space
|
||||||
|
func (h *StorageHandler) GetObjectsInStorage(c echo.Context) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Parse storage ID from URL
|
||||||
|
storageIDStr := c.Param("id")
|
||||||
|
storageID, err := strconv.ParseInt(storageIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("Invalid storage ID", zap.String("id", storageIDStr))
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||||
|
"error": "Invalid storage ID",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if storage exists
|
||||||
|
_, err = h.db.GetStorageSpaceByID(ctx, storageID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{
|
||||||
|
"error": "Storage space not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
h.logger.Error("Failed to fetch storage space", zap.Error(err))
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
||||||
|
"error": "Failed to fetch storage space",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get objects in this storage
|
||||||
|
objects, err := h.db.GetObjectsByStorageID(ctx, storageID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("Failed to fetch objects in storage", zap.Error(err))
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
||||||
|
"error": "Failed to fetch objects in storage",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to a format suitable for the frontend
|
||||||
|
result := make([]map[string]interface{}, len(objects))
|
||||||
|
for i, obj := range objects {
|
||||||
|
result[i] = map[string]interface{}{
|
||||||
|
"id": obj.ID,
|
||||||
|
"name": obj.Name,
|
||||||
|
"storagespaceId": obj.StoragespaceID,
|
||||||
|
"description": obj.Description.String,
|
||||||
|
"serialnumber": obj.Serialnumber.String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStorageHierarchyObjects returns all objects in a storage hierarchy
|
||||||
|
func (h *StorageHandler) GetStorageHierarchyObjects(c echo.Context) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Parse root storage ID from URL
|
||||||
|
rootStorageIDStr := c.Param("id")
|
||||||
|
rootStorageID, err := strconv.ParseInt(rootStorageIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("Invalid storage ID", zap.String("id", rootStorageIDStr))
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||||
|
"error": "Invalid storage ID",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if root storage exists
|
||||||
|
_, err = h.db.GetStorageSpaceByID(ctx, rootStorageID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{
|
||||||
|
"error": "Storage space not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
h.logger.Error("Failed to fetch storage space", zap.Error(err))
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
||||||
|
"error": "Failed to fetch storage space",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all storage spaces to build the hierarchy
|
||||||
|
allStorageSpaces, err := h.db.GetAllStorageSpaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("Failed to fetch all storage spaces", zap.Error(err))
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
||||||
|
"error": "Failed to fetch storage hierarchy",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a list of all storage IDs in the hierarchy
|
||||||
|
storageIDs := []int64{rootStorageID}
|
||||||
|
storageIDs = append(storageIDs, h.getChildStorageIDs(allStorageSpaces, rootStorageID)...)
|
||||||
|
|
||||||
|
// Get objects for all storage spaces in the hierarchy
|
||||||
|
var allObjects []map[string]interface{}
|
||||||
|
|
||||||
|
for _, storageID := range storageIDs {
|
||||||
|
// Get objects in this storage
|
||||||
|
objects, err := h.db.GetObjectsByStorageID(ctx, storageID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("Failed to fetch objects in storage",
|
||||||
|
zap.Int64("storageID", storageID),
|
||||||
|
zap.Error(err))
|
||||||
|
continue // Skip this storage if there's an error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to a format suitable for the frontend
|
||||||
|
for _, obj := range objects {
|
||||||
|
allObjects = append(allObjects, map[string]interface{}{
|
||||||
|
"id": obj.ID,
|
||||||
|
"name": obj.Name,
|
||||||
|
"storagespaceId": obj.StoragespaceID,
|
||||||
|
"description": obj.Description.String,
|
||||||
|
"serialnumber": obj.Serialnumber.String,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, allObjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to recursively get all child storage IDs
|
||||||
|
func (h *StorageHandler) getChildStorageIDs(spaces []db.Storagespace, parentID int64) []int64 {
|
||||||
|
var childIDs []int64
|
||||||
|
|
||||||
|
for _, space := range spaces {
|
||||||
|
if space.Parent.Valid && space.Parent.Int64 == parentID {
|
||||||
|
childIDs = append(childIDs, space.ID)
|
||||||
|
// Recursively get children of this child
|
||||||
|
childIDs = append(childIDs, h.getChildStorageIDs(spaces, space.ID)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return childIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes registers all storage-related routes
|
||||||
|
func (h *StorageHandler) RegisterRoutes(g *echo.Group) {
|
||||||
|
g.GET("/storageSpaces", h.GetStorageSpaces)
|
||||||
|
g.GET("/storageSpaces/:id/objects", h.GetObjectsInStorage)
|
||||||
|
g.GET("/storageSpaces/:id/hierarchy/objects", h.GetStorageHierarchyObjects)
|
||||||
|
}
|
|
@ -1,55 +1,69 @@
|
||||||
-- name: GetAllUsers :many
|
-- name: GetAllUsers :many
|
||||||
SELECT * FROM users;
|
SELECT id, username FROM users;
|
||||||
|
|
||||||
-- name: GetUserByID :one
|
-- name: GetUserByID :one
|
||||||
SELECT * FROM users WHERE ID = ?;
|
SELECT id, username FROM users WHERE id = ?;
|
||||||
|
|
||||||
-- name: AddUser :exec
|
-- name: AddUser :exec
|
||||||
INSERT INTO users (Username) VALUES (?);
|
INSERT INTO users (username) VALUES (?);
|
||||||
|
|
||||||
-- name: GetUserSessions :many
|
-- name: GetUserSessions :many
|
||||||
SELECT * FROM user_sessions_view WHERE user_ID = ?;
|
SELECT user_id, username, session_token, created_at, valid_until FROM user_sessions_view WHERE user_id = ?;
|
||||||
|
|
||||||
-- name: AddUserSession :exec
|
-- name: AddUserSession :exec
|
||||||
INSERT INTO user_sessions (user_ID, session_token, created_at, valid_until, name)
|
INSERT INTO user_sessions (user_id, session_token, created_at, valid_until, name)
|
||||||
VALUES (?, ?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?);
|
||||||
|
|
||||||
-- name: GetObjectsInStorage :many
|
-- name: GetObjectsInStorage :many
|
||||||
SELECT * FROM object_storage WHERE storage_location = ?;
|
SELECT object_id, object_name, storage_location FROM object_storage WHERE storage_location = ?;
|
||||||
|
|
||||||
-- name: GetUserEvents :many
|
-- name: GetUserEvents :many
|
||||||
SELECT * FROM event_details WHERE organizer = ?;
|
SELECT event_id, event_name, description, location, start_date, end_date, organizer FROM event_details WHERE organizer = ?;
|
||||||
|
|
||||||
-- name: GetEventObjects :many
|
-- name: GetEventObjects :many
|
||||||
SELECT objects.* FROM objects
|
SELECT objects.id, objects.storagespace_id, objects.name, objects.description, objects.serialnumber, objects.created
|
||||||
JOIN events_objects ON objects.ID = events_objects.object_ID
|
FROM objects
|
||||||
WHERE events_objects.event_ID = ?;
|
JOIN events_objects ON objects.id = events_objects.object_id
|
||||||
|
WHERE events_objects.event_id = ?;
|
||||||
|
|
||||||
-- name: AddObject :exec
|
-- name: AddObject :exec
|
||||||
INSERT INTO objects (storagespace_ID, Name, Description, Serialnumber, created)
|
INSERT INTO objects (storagespace_id, name, description, serialnumber, created)
|
||||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP);
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP);
|
||||||
|
|
||||||
-- name: CreateEvent :exec
|
-- name: CreateEvent :exec
|
||||||
INSERT INTO events (user_ID, Name, Description, Location, Start_Date, End_Date)
|
INSERT INTO events (user_id, name, description, location, start_date, end_date)
|
||||||
VALUES (?, ?, ?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
-- name: GetEventCheckIns :many
|
-- name: GetEventCheckIns :many
|
||||||
SELECT * FROM check_in_log WHERE event_name = ?;
|
SELECT check_in_id, username, event_name, object_name, datetime, checkin_state FROM check_in_log WHERE event_name = ?;
|
||||||
|
|
||||||
-- name: AddCheckIn :exec
|
-- name: AddCheckIn :exec
|
||||||
INSERT INTO check_in (user_ID, checkin_event_ID, event_ID, object_ID, checkin_state_ID, datetime)
|
INSERT INTO check_in (user_id, checkin_event_id, event_id, object_id, checkin_state_id, datetime)
|
||||||
VALUES (?, ?, ?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
-- name: GetObjectAnnotations :many
|
-- name: GetObjectAnnotations :many
|
||||||
SELECT * FROM annotations WHERE object_ID = ?;
|
SELECT id, user_id, object_id, event_id, check_in_id, events_object_id, text, datetime FROM annotations WHERE object_id = ?;
|
||||||
|
|
||||||
-- name: AddAnnotation :exec
|
-- name: AddAnnotation :exec
|
||||||
INSERT INTO annotations (user_ID, object_ID, event_ID, check_in_ID, events_object_ID, text, datetime)
|
INSERT INTO annotations (user_id, object_id, event_id, check_in_id, events_object_id, text, datetime)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
-- name: GetEventPictures :many
|
-- name: GetEventPictures :many
|
||||||
SELECT * FROM pictures WHERE event_ID = ?;
|
SELECT id, user_id, storagespace_id, object_id, event_id, check_in_id, path, description, datetime FROM pictures WHERE event_id = ?;
|
||||||
|
|
||||||
-- name: AddPicture :exec
|
-- name: AddPicture :exec
|
||||||
INSERT INTO pictures (user_ID, storagespace_ID, object_ID, event_ID, check_in_ID, Path, Description, datetime)
|
INSERT INTO pictures (user_id, storagespace_id, object_id, event_id, check_in_id, path, description, datetime)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
-- name: GetAllStorageSpaces :many
|
||||||
|
SELECT id, parent, location FROM storagespace;
|
||||||
|
|
||||||
|
-- name: GetStorageSpaceByID :one
|
||||||
|
SELECT id, parent, location FROM storagespace WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: GetChildStorageSpaces :many
|
||||||
|
SELECT id, parent, location FROM storagespace WHERE parent = ?;
|
||||||
|
|
||||||
|
-- name: GetObjectsByStorageID :many
|
||||||
|
SELECT id, storagespace_id, name, description, serialnumber, created
|
||||||
|
FROM objects WHERE storagespace_id = ?;
|
||||||
|
|
226
main.go
226
main.go
|
@ -5,18 +5,25 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/num30/config"
|
|
||||||
"github.com/pressly/goose/v3"
|
"github.com/pressly/goose/v3"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/your-username/your-repo/api"
|
||||||
|
db "github.com/your-username/your-repo/database/sqlite/generated"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed static/*
|
//go:embed static/*
|
||||||
|
@ -26,11 +33,11 @@ var staticFiles embed.FS
|
||||||
var sqliteMigrations embed.FS
|
var sqliteMigrations embed.FS
|
||||||
|
|
||||||
type OAuthConfig struct {
|
type OAuthConfig struct {
|
||||||
ClientID string `config:"client_id"`
|
ClientID string `config:"client_id" yaml:"client_id"`
|
||||||
ClientSecret string `config:"client_secret"`
|
ClientSecret string `config:"client_secret" yaml:"client_secret"`
|
||||||
AuthURL string `config:"auth_url"`
|
AuthURL string `config:"auth_url" yaml:"auth_url"`
|
||||||
TokenURL string `config:"token_url"`
|
TokenURL string `config:"token_url" yaml:"token_url"`
|
||||||
UserInfoURL string `config:"userinfo_url"`
|
UserInfoURL string `config:"user_info_url" yaml:"user_info_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DBConfig struct {
|
type DBConfig struct {
|
||||||
|
@ -48,7 +55,7 @@ type Config struct {
|
||||||
Server struct {
|
Server struct {
|
||||||
Port int `config:"port"`
|
Port int `config:"port"`
|
||||||
} `config:"server"`
|
} `config:"server"`
|
||||||
OAuth OAuthConfig `config:"oauth"`
|
OAuth OAuthConfig `config:"oauth" yaml:"oauth"`
|
||||||
DB DBConfig `config:"db"`
|
DB DBConfig `config:"db"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,29 +65,98 @@ var (
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *sql.DB
|
var sqlDB *sql.DB
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var cfg Config
|
// Load configuration from YAML file, environment variables, and flags
|
||||||
|
configFile := flag.String("config", "config.yaml", "Path to config file")
|
||||||
|
serverPort := flag.Int("port", 0, "Server port (overrides config file)")
|
||||||
|
oauthClientID := flag.String("oauth-client-id", "", "OAuth Client ID (overrides config file)")
|
||||||
|
oauthClientSecret := flag.String("oauth-client-secret", "", "OAuth Client Secret (overrides config file)")
|
||||||
|
oauthAuthURL := flag.String("oauth-auth-url", "", "OAuth Auth URL (overrides config file)")
|
||||||
|
oauthTokenURL := flag.String("oauth-token-url", "", "OAuth Token URL (overrides config file)")
|
||||||
|
oauthUserInfoURL := flag.String("oauth-user-info-url", "", "OAuth User Info URL (overrides config file)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
cfgReader := config.NewConfReader("config").WithSearchDirs("./", ".")
|
// Set default values
|
||||||
cfgErr := cfgReader.Read(&cfg)
|
cfg = Config{
|
||||||
|
Env: "dev",
|
||||||
|
Server: struct {
|
||||||
|
Port int `config:"port"`
|
||||||
|
}{Port: 8088},
|
||||||
|
OAuth: OAuthConfig{},
|
||||||
|
DB: DBConfig{
|
||||||
|
Driver: "sqlite3",
|
||||||
|
DBName: "./inventor.sqlite",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlData, err := os.ReadFile(*configFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: Could not read config file: %v\n", err)
|
||||||
|
} else {
|
||||||
|
if err := yaml.Unmarshal(yamlData, &cfg); err != nil {
|
||||||
|
fmt.Printf("Warning: Could not parse config file: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Successfully loaded configuration from", *configFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if envPort := os.Getenv("INVENTOR_SERVER_PORT"); envPort != "" {
|
||||||
|
if port, err := strconv.Atoi(envPort); err == nil {
|
||||||
|
cfg.Server.Port = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if envClientID := os.Getenv("INVENTOR_OAUTH_CLIENT_ID"); envClientID != "" {
|
||||||
|
cfg.OAuth.ClientID = envClientID
|
||||||
|
}
|
||||||
|
if envClientSecret := os.Getenv("INVENTOR_OAUTH_CLIENT_SECRET"); envClientSecret != "" {
|
||||||
|
cfg.OAuth.ClientSecret = envClientSecret
|
||||||
|
}
|
||||||
|
if envAuthURL := os.Getenv("INVENTOR_OAUTH_AUTH_URL"); envAuthURL != "" {
|
||||||
|
cfg.OAuth.AuthURL = envAuthURL
|
||||||
|
}
|
||||||
|
if envTokenURL := os.Getenv("INVENTOR_OAUTH_TOKEN_URL"); envTokenURL != "" {
|
||||||
|
cfg.OAuth.TokenURL = envTokenURL
|
||||||
|
}
|
||||||
|
if envUserInfoURL := os.Getenv("INVENTOR_OAUTH_USER_INFO_URL"); envUserInfoURL != "" {
|
||||||
|
cfg.OAuth.UserInfoURL = envUserInfoURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Override with command line flags (highest priority)
|
||||||
|
if *serverPort != 0 {
|
||||||
|
cfg.Server.Port = *serverPort
|
||||||
|
}
|
||||||
|
if *oauthClientID != "" {
|
||||||
|
cfg.OAuth.ClientID = *oauthClientID
|
||||||
|
}
|
||||||
|
if *oauthClientSecret != "" {
|
||||||
|
cfg.OAuth.ClientSecret = *oauthClientSecret
|
||||||
|
}
|
||||||
|
if *oauthAuthURL != "" {
|
||||||
|
cfg.OAuth.AuthURL = *oauthAuthURL
|
||||||
|
}
|
||||||
|
if *oauthTokenURL != "" {
|
||||||
|
cfg.OAuth.TokenURL = *oauthTokenURL
|
||||||
|
}
|
||||||
|
if *oauthUserInfoURL != "" {
|
||||||
|
cfg.OAuth.UserInfoURL = *oauthUserInfoURL
|
||||||
|
}
|
||||||
|
|
||||||
zapcfg := zap.NewProductionConfig()
|
zapcfg := zap.NewProductionConfig()
|
||||||
if cfg.Env == "dev" {
|
if cfg.Env == "dev" {
|
||||||
zapcfg = zap.NewDevelopmentConfig()
|
zapcfg = zap.NewDevelopmentConfig()
|
||||||
}
|
}
|
||||||
logger, err := zapcfg.Build()
|
logger, err = zapcfg.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("Failed to initialize logger: %v", err))
|
panic(fmt.Sprintf("Failed to initialize logger: %v", err))
|
||||||
}
|
}
|
||||||
defer logger.Sync()
|
defer logger.Sync()
|
||||||
|
|
||||||
if cfgErr != nil {
|
// Configuration errors are now handled inline during loading
|
||||||
logger.Fatal("Failed to load config", zap.Error(cfgErr))
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Loaded config", zap.Any("config", cfg))
|
// Print the raw config to see what's actually loaded
|
||||||
|
logger.Debug("Raw config loaded", zap.Any("config", cfg))
|
||||||
|
|
||||||
// Initialize OAuth2
|
// Initialize OAuth2
|
||||||
oauthConfig = &oauth2.Config{
|
oauthConfig = &oauth2.Config{
|
||||||
|
@ -90,10 +166,17 @@ func main() {
|
||||||
AuthURL: cfg.OAuth.AuthURL,
|
AuthURL: cfg.OAuth.AuthURL,
|
||||||
TokenURL: cfg.OAuth.TokenURL,
|
TokenURL: cfg.OAuth.TokenURL,
|
||||||
},
|
},
|
||||||
RedirectURL: fmt.Sprintf("http://localhost:%d/oauth/callback", cfg.Server.Port),
|
RedirectURL: fmt.Sprintf("http://localhost:3000/oauth/callback"),
|
||||||
Scopes: []string{"profile", "email"},
|
Scopes: []string{"profile", "email", "openid"},
|
||||||
}
|
}
|
||||||
fmt.Println(oauthConfig)
|
|
||||||
|
// Log the OAuth config that's actually being used
|
||||||
|
logger.Info("Initialized OAuth Config",
|
||||||
|
zap.String("ClientID", oauthConfig.ClientID),
|
||||||
|
zap.String("ClientSecret", oauthConfig.ClientSecret),
|
||||||
|
zap.String("AuthURL", oauthConfig.Endpoint.AuthURL),
|
||||||
|
zap.String("TokenURL", oauthConfig.Endpoint.TokenURL),
|
||||||
|
zap.String("RedirectURL", oauthConfig.RedirectURL))
|
||||||
|
|
||||||
// Initialize database connection
|
// Initialize database connection
|
||||||
var dsn string
|
var dsn string
|
||||||
|
@ -106,25 +189,28 @@ func main() {
|
||||||
cfg.DB.User, cfg.DB.Password, cfg.DB.Host, cfg.DB.Port, cfg.DB.DBName)
|
cfg.DB.User, cfg.DB.Password, cfg.DB.Host, cfg.DB.Port, cfg.DB.DBName)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err = sql.Open(cfg.DB.Driver, dsn)
|
sqlDB, err = sql.Open(cfg.DB.Driver, dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Failed to connect to database", zap.Error(err))
|
logger.Fatal("Failed to connect to database", zap.Error(err))
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer sqlDB.Close()
|
||||||
|
|
||||||
// Test the connection
|
// Test the connection
|
||||||
if err = db.Ping(); err != nil {
|
if err = sqlDB.Ping(); err != nil {
|
||||||
logger.Fatal("Failed to ping database", zap.Error(err))
|
logger.Fatal("Failed to ping database", zap.Error(err))
|
||||||
}
|
}
|
||||||
logger.Info("Successfully connected to database", zap.String("driver", cfg.DB.Driver))
|
logger.Info("Successfully connected to database", zap.String("driver", cfg.DB.Driver))
|
||||||
|
|
||||||
if cfg.DB.Driver == "sqlite3" {
|
if cfg.DB.Driver == "sqlite3" {
|
||||||
logger.Info("migrating sqlite database")
|
logger.Info("migrating sqlite database")
|
||||||
if err := migrateSqlite(db); err != nil {
|
if err := migrateSqlite(sqlDB); err != nil {
|
||||||
logger.Fatal("Failed to migrate database", zap.Error(err))
|
logger.Fatal("Failed to migrate database", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize database queries
|
||||||
|
dbQueries := db.New(sqlDB)
|
||||||
|
|
||||||
// Initialize Echo
|
// Initialize Echo
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.Use(middleware.Logger())
|
e.Use(middleware.Logger())
|
||||||
|
@ -135,9 +221,12 @@ func main() {
|
||||||
e.GET("/oauth/login", handleOAuthLogin)
|
e.GET("/oauth/login", handleOAuthLogin)
|
||||||
e.GET("/oauth/callback", handleOAuthCallback)
|
e.GET("/oauth/callback", handleOAuthCallback)
|
||||||
|
|
||||||
|
// Initialize API
|
||||||
|
apiHandler := api.New(dbQueries, logger)
|
||||||
|
|
||||||
// Protected API routes
|
// Protected API routes
|
||||||
api := e.Group("/api/v1", oauthMiddleware)
|
apiGroup := e.Group("/api/v1", oauthMiddleware)
|
||||||
api.GET("/health", func(c echo.Context) error {
|
apiGroup.GET("/health", func(c echo.Context) error {
|
||||||
user := c.Get("user").(map[string]interface{})
|
user := c.Get("user").(map[string]interface{})
|
||||||
logger.Info("Health check requested", zap.Any("user", user))
|
logger.Info("Health check requested", zap.Any("user", user))
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
@ -146,6 +235,9 @@ func main() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Register all other API routes
|
||||||
|
apiHandler.RegisterRoutes(e, oauthMiddleware)
|
||||||
|
|
||||||
// Serve static files - must be after API routes
|
// Serve static files - must be after API routes
|
||||||
e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles))))
|
e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles))))
|
||||||
|
|
||||||
|
@ -194,6 +286,15 @@ func handleOAuthCallback(c echo.Context) error {
|
||||||
|
|
||||||
logger.Info("OAuth token exchanged successfully", zap.String("access_token", token.AccessToken))
|
logger.Info("OAuth token exchanged successfully", zap.String("access_token", token.AccessToken))
|
||||||
|
|
||||||
|
// Fetch user info to verify the token works
|
||||||
|
userInfo, err := fetchUserInfo(token.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to fetch user info after token exchange", zap.Error(err))
|
||||||
|
// Continue anyway, as the token might still be valid
|
||||||
|
} else {
|
||||||
|
logger.Info("Successfully fetched user info after token exchange", zap.Any("userInfo", userInfo))
|
||||||
|
}
|
||||||
|
|
||||||
// Return HTML that stores the token and redirects to the main app
|
// Return HTML that stores the token and redirects to the main app
|
||||||
html := fmt.Sprintf(`
|
html := fmt.Sprintf(`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
@ -230,10 +331,30 @@ func oauthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid Authorization header"})
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid Authorization header"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the UserInfoURL being used
|
||||||
|
logger.Info("Fetching user info", zap.String("UserInfoURL", cfg.OAuth.UserInfoURL))
|
||||||
|
|
||||||
// Validate token and fetch user info
|
// Validate token and fetch user info
|
||||||
userInfo, err := fetchUserInfo(token)
|
userInfo, err := fetchUserInfo(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("OAuth token validation failed", zap.Error(err))
|
logger.Error("OAuth token validation failed", zap.Error(err))
|
||||||
|
|
||||||
|
// For debugging purposes, try to get more information about the error
|
||||||
|
logger.Debug("Attempting to decode token for debugging", zap.String("token", token))
|
||||||
|
|
||||||
|
// Create a mock user for development purposes if in dev mode
|
||||||
|
if cfg.Env == "dev" {
|
||||||
|
logger.Warn("DEV MODE: Using mock user due to token validation failure")
|
||||||
|
mockUser := map[string]interface{}{
|
||||||
|
"sub": "mock-user-id",
|
||||||
|
"name": "Mock User",
|
||||||
|
"email": "mock@example.com",
|
||||||
|
"preferred_username": "mockuser",
|
||||||
|
}
|
||||||
|
c.Set("user", mockUser)
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid token"})
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid token"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,27 +366,64 @@ func oauthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
|
||||||
// Fetch user info from OAuth provider
|
// Fetch user info from OAuth provider
|
||||||
func fetchUserInfo(token string) (map[string]interface{}, error) {
|
func fetchUserInfo(token string) (map[string]interface{}, error) {
|
||||||
|
if cfg.OAuth.UserInfoURL == "" {
|
||||||
|
return nil, fmt.Errorf("UserInfoURL is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new request
|
||||||
req, err := http.NewRequest("GET", cfg.OAuth.UserInfoURL, nil)
|
req, err := http.NewRequest("GET", cfg.OAuth.UserInfoURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
|
|
||||||
client := http.Client{}
|
// Set headers - try both common authorization methods
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
// Log the full request for debugging
|
||||||
|
logger.Debug("UserInfo request",
|
||||||
|
zap.String("url", req.URL.String()),
|
||||||
|
zap.String("method", req.Method),
|
||||||
|
zap.Any("headers", req.Header))
|
||||||
|
|
||||||
|
// Create a client with reasonable timeout
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the request
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
// Read the response body
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
return nil, fmt.Errorf("OAuth validation failed: %s", string(body))
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the response for debugging
|
||||||
|
logger.Debug("UserInfo response",
|
||||||
|
zap.Int("status", resp.StatusCode),
|
||||||
|
zap.String("body", string(body)),
|
||||||
|
zap.Any("headers", resp.Header))
|
||||||
|
|
||||||
|
// Check for successful status code
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("OAuth validation failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON response
|
||||||
var userInfo map[string]interface{}
|
var userInfo map[string]interface{}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
if err := json.Unmarshal(body, &userInfo); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to parse user info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we have some basic user information
|
||||||
|
if userInfo["sub"] == nil && userInfo["id"] == nil && userInfo["email"] == nil {
|
||||||
|
return nil, fmt.Errorf("response doesn't contain expected user information")
|
||||||
}
|
}
|
||||||
|
|
||||||
return userInfo, nil
|
return userInfo, nil
|
||||||
|
|
193
web/src/components/StorageHierarchy.vue
Normal file
193
web/src/components/StorageHierarchy.vue
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Storage Hierarchy
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-select
|
||||||
|
v-model="selectedStorageId"
|
||||||
|
:items="storageSpaces"
|
||||||
|
item-title="location"
|
||||||
|
item-value="id"
|
||||||
|
label="Select Storage Space"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
class="ml-2"
|
||||||
|
style="max-width: 300px"
|
||||||
|
></v-select>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-alert
|
||||||
|
v-if="error"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
closable
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<div v-if="loading" class="d-flex justify-center align-center my-4">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-expansion-panels v-if="selectedStorage">
|
||||||
|
<storage-box
|
||||||
|
:storage="selectedStorage"
|
||||||
|
:objects="objectsInStorage"
|
||||||
|
:nested-storages="nestedStorages"
|
||||||
|
/>
|
||||||
|
</v-expansion-panels>
|
||||||
|
|
||||||
|
<v-alert v-else type="info" variant="tonal">
|
||||||
|
Please select a storage space to view its contents
|
||||||
|
</v-alert>
|
||||||
|
</template>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch, onMounted, defineComponent } from 'vue';
|
||||||
|
import { useStorageStore } from '@/stores/storage';
|
||||||
|
|
||||||
|
// Component for recursive display of storage boxes
|
||||||
|
const StorageBox = defineComponent({
|
||||||
|
name: 'StorageBox',
|
||||||
|
props: {
|
||||||
|
storage: Object,
|
||||||
|
objects: Array,
|
||||||
|
nestedStorages: Array
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const objectsInCurrentStorage = computed(() => {
|
||||||
|
return props.objects.filter(obj => obj.storagespaceId === props.storage.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const childStorages = computed(() => {
|
||||||
|
return props.nestedStorages.filter(storage =>
|
||||||
|
storage.parent && storage.parent.valid && storage.parent.int64 === props.storage.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<v-expansion-panel>
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2">mdi-package-variant-closed</v-icon>
|
||||||
|
<span>{ props.storage.location || `Storage #${props.storage.id}` }</span>
|
||||||
|
<v-chip class="ml-2" size="small" color="primary" variant="outlined">
|
||||||
|
{ objectsInCurrentStorage.value.length } objects
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-subtitle-1 mb-2">Objects in this storage:</h3>
|
||||||
|
{objectsInCurrentStorage.value.length > 0 ? (
|
||||||
|
<v-list lines="two">
|
||||||
|
{objectsInCurrentStorage.value.map(obj => (
|
||||||
|
<v-list-item
|
||||||
|
:key="obj.id"
|
||||||
|
:title="obj.name"
|
||||||
|
:subtitle="obj.description || 'No description'"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-cube-outline</v-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-chip size="small" color="grey" variant="flat">
|
||||||
|
SN: { obj.serialnumber || 'N/A' }
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
))}
|
||||||
|
</v-list>
|
||||||
|
) : (
|
||||||
|
<v-alert type="info" variant="tonal" density="compact">
|
||||||
|
No objects in this storage
|
||||||
|
</v-alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{childStorages.value.length > 0 && (
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-subtitle-1 mb-2">Nested storage spaces:</h3>
|
||||||
|
<v-expansion-panels>
|
||||||
|
{childStorages.value.map(childStorage => (
|
||||||
|
<StorageBox
|
||||||
|
key={childStorage.id}
|
||||||
|
storage={childStorage}
|
||||||
|
objects={props.objects}
|
||||||
|
nestedStorages={props.nestedStorages}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</v-expansion-panels>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main component logic
|
||||||
|
const storageStore = useStorageStore();
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref(null);
|
||||||
|
const selectedStorageId = ref(null);
|
||||||
|
|
||||||
|
const storageSpaces = computed(() => storageStore.storageSpaces);
|
||||||
|
const objects = computed(() => storageStore.objects);
|
||||||
|
|
||||||
|
const selectedStorage = computed(() => {
|
||||||
|
if (!selectedStorageId.value) return null;
|
||||||
|
return storageSpaces.value.find(s => s.id === selectedStorageId.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectsInStorage = computed(() => {
|
||||||
|
if (!selectedStorageId.value) return [];
|
||||||
|
return objects.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nestedStorages = computed(() => {
|
||||||
|
return storageSpaces.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(selectedStorageId, async (newId) => {
|
||||||
|
if (newId) {
|
||||||
|
await fetchStorageData(newId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchStorageData(storageId) {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storageStore.fetchStorageHierarchy(storageId);
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message || 'Failed to load storage data';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storageStore.fetchStorageSpaces();
|
||||||
|
if (storageSpaces.value.length > 0) {
|
||||||
|
selectedStorageId.value = storageSpaces.value[0].id;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message || 'Failed to load storage spaces';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,7 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<HelloWorld />
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<h1 class="text-h4 mb-4">Inventory Management System</h1>
|
||||||
|
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<v-card-text>
|
||||||
|
<p>Welcome to the Inventory Management System. Use this dashboard to view and manage objects in storage spaces.</p>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<StorageHierarchy />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
//
|
import StorageHierarchy from '@/components/StorageHierarchy.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,10 +5,13 @@ export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
token: localStorage.getItem('auth_token') || null,
|
token: localStorage.getItem('auth_token') || null,
|
||||||
user: null as any | null,
|
user: null as any | null,
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
isAuthenticated: (state) => !!state.token,
|
isAuthenticated: (state) => !!state.token,
|
||||||
|
isLoading: (state) => state.loading,
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -21,32 +24,60 @@ export const useAuthStore = defineStore('auth', {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setError(error: string | null) {
|
||||||
|
this.error = error;
|
||||||
|
},
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.token = null;
|
this.token = null;
|
||||||
this.user = null;
|
this.user = null;
|
||||||
|
this.error = null;
|
||||||
localStorage.removeItem('auth_token');
|
localStorage.removeItem('auth_token');
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchUserInfo() {
|
async fetchUserInfo() {
|
||||||
if (!this.token) return;
|
if (!this.token) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/health', {
|
const response = await fetch('/api/v1/health', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.token}`
|
'Authorization': `Bearer ${this.token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.setUser(data.user);
|
this.setUser(data.user);
|
||||||
|
console.log('User info fetched successfully:', data.user);
|
||||||
} else {
|
} else {
|
||||||
// If token is invalid, logout
|
// Get error details
|
||||||
this.logout();
|
let errorText = 'Authentication failed';
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorText = errorData.error || errorText;
|
||||||
|
} catch (e) {
|
||||||
|
// If we can't parse the error as JSON, use the status text
|
||||||
|
errorText = response.statusText || errorText;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Failed to fetch user info:', errorText);
|
||||||
|
this.setError(errorText);
|
||||||
|
|
||||||
|
// If token is invalid (401), logout
|
||||||
|
if (response.status === 401) {
|
||||||
|
this.logout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch user info:', error);
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
this.logout();
|
console.error('Failed to fetch user info:', errorMessage);
|
||||||
|
this.setError(errorMessage);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
113
web/src/stores/storage.ts
Normal file
113
web/src/stores/storage.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useStorageStore = defineStore('storage', {
|
||||||
|
state: () => ({
|
||||||
|
storageSpaces: [] as any[],
|
||||||
|
objects: [] as any[],
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
getStorageById: (state) => (id: number) => {
|
||||||
|
return state.storageSpaces.find(storage => storage.id === id);
|
||||||
|
},
|
||||||
|
|
||||||
|
getObjectsInStorage: (state) => (storageId: number) => {
|
||||||
|
return state.objects.filter(obj => obj.storagespaceId === storageId);
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildStorages: (state) => (parentId: number) => {
|
||||||
|
return state.storageSpaces.filter(storage =>
|
||||||
|
storage.parent && storage.parent === parentId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchStorageSpaces() {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/storageSpaces', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch storage spaces');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.storageSpaces = data.map((space: any) => ({
|
||||||
|
id: space.id,
|
||||||
|
parent: space.parent,
|
||||||
|
location: space.location
|
||||||
|
}));
|
||||||
|
} catch (error: any) {
|
||||||
|
this.error = error.message;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchObjectsInStorage(storageId: number) {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/storageSpaces/${storageId}/objects`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch objects');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.objects = data;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.error = error.message;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchStorageHierarchy(rootStorageId: number) {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, ensure we have all storage spaces
|
||||||
|
if (this.storageSpaces.length === 0) {
|
||||||
|
await this.fetchStorageSpaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then fetch all objects in the hierarchy
|
||||||
|
const response = await fetch(`/api/v1/storageSpaces/${rootStorageId}/hierarchy/objects`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch storage hierarchy');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.objects = data;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.error = error.message;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue