Store configuration owner by MariaDB username
This commit is contained in:
@@ -77,10 +77,13 @@ func main() {
|
|||||||
if *dryRun {
|
if *dryRun {
|
||||||
log.Println("\n[DRY RUN] Would migrate the following configurations:")
|
log.Println("\n[DRY RUN] Would migrate the following configurations:")
|
||||||
for _, c := range configs {
|
for _, c := range configs {
|
||||||
userName := "unknown"
|
userName := c.OwnerUsername
|
||||||
if c.User != nil {
|
if userName == "" && c.User != nil {
|
||||||
userName = c.User.Username
|
userName = c.User.Username
|
||||||
}
|
}
|
||||||
|
if userName == "" {
|
||||||
|
userName = "unknown"
|
||||||
|
}
|
||||||
log.Printf(" - %s (UUID: %s, User: %s, Items: %d)", c.Name, c.UUID, userName, len(c.Items))
|
log.Printf(" - %s (UUID: %s, User: %s, Items: %d)", c.Name, c.UUID, userName, len(c.Items))
|
||||||
}
|
}
|
||||||
log.Printf("\nTotal: %d configurations", len(configs))
|
log.Printf("\nTotal: %d configurations", len(configs))
|
||||||
@@ -115,20 +118,25 @@ func main() {
|
|||||||
// Create local configuration
|
// Create local configuration
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
localConfig := &localdb.LocalConfiguration{
|
localConfig := &localdb.LocalConfiguration{
|
||||||
UUID: c.UUID,
|
UUID: c.UUID,
|
||||||
ServerID: &c.ID,
|
ServerID: &c.ID,
|
||||||
Name: c.Name,
|
Name: c.Name,
|
||||||
Items: localItems,
|
Items: localItems,
|
||||||
TotalPrice: c.TotalPrice,
|
TotalPrice: c.TotalPrice,
|
||||||
CustomPrice: c.CustomPrice,
|
CustomPrice: c.CustomPrice,
|
||||||
Notes: c.Notes,
|
Notes: c.Notes,
|
||||||
IsTemplate: c.IsTemplate,
|
IsTemplate: c.IsTemplate,
|
||||||
ServerCount: c.ServerCount,
|
ServerCount: c.ServerCount,
|
||||||
CreatedAt: c.CreatedAt,
|
CreatedAt: c.CreatedAt,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
SyncedAt: &now,
|
SyncedAt: &now,
|
||||||
SyncStatus: "synced",
|
SyncStatus: "synced",
|
||||||
OriginalUserID: c.UserID,
|
OriginalUserID: c.UserID,
|
||||||
|
OriginalUsername: c.OwnerUsername,
|
||||||
|
}
|
||||||
|
|
||||||
|
if localConfig.OriginalUsername == "" && c.User != nil {
|
||||||
|
localConfig.OriginalUsername = c.User.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := local.SaveConfiguration(localConfig); err != nil {
|
if err := local.SaveConfiguration(localConfig); err != nil {
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ func main() {
|
|||||||
connMgr := db.NewConnectionManager(local)
|
connMgr := db.NewConnectionManager(local)
|
||||||
|
|
||||||
dbUser := local.GetDBUser()
|
dbUser := local.GetDBUser()
|
||||||
dbUserID := uint(1)
|
|
||||||
|
|
||||||
// Try to connect to MariaDB on startup
|
// Try to connect to MariaDB on startup
|
||||||
mariaDB, err := connMgr.GetDB()
|
mariaDB, err := connMgr.GetDB()
|
||||||
@@ -136,12 +135,6 @@ func main() {
|
|||||||
mariaDB = nil
|
mariaDB = nil
|
||||||
} else {
|
} else {
|
||||||
slog.Info("successfully connected to MariaDB on startup")
|
slog.Info("successfully connected to MariaDB on startup")
|
||||||
// Ensure DB user exists and get their ID
|
|
||||||
if dbUserID, err = models.EnsureDBUser(mariaDB, dbUser); err != nil {
|
|
||||||
slog.Error("failed to ensure DB user", "error", err)
|
|
||||||
// Continue with default ID
|
|
||||||
dbUserID = uint(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("starting QuoteForge server",
|
slog.Info("starting QuoteForge server",
|
||||||
@@ -149,7 +142,6 @@ func main() {
|
|||||||
"host", cfg.Server.Host,
|
"host", cfg.Server.Host,
|
||||||
"port", cfg.Server.Port,
|
"port", cfg.Server.Port,
|
||||||
"db_user", dbUser,
|
"db_user", dbUser,
|
||||||
"db_user_id", dbUserID,
|
|
||||||
"online", mariaDB != nil,
|
"online", mariaDB != nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -171,7 +163,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(cfg.Server.Mode)
|
gin.SetMode(cfg.Server.Mode)
|
||||||
router, syncService, err := setupRouter(cfg, local, connMgr, mariaDB, dbUserID)
|
router, syncService, err := setupRouter(cfg, local, connMgr, mariaDB, dbUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to setup router", "error", err)
|
slog.Error("failed to setup router", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -400,7 +392,7 @@ func setupDatabaseFromDSN(dsn string) (*gorm.DB, error) {
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUserID uint) (*gin.Engine, *sync.Service, error) {
|
func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUsername string) (*gin.Engine, *sync.Service, error) {
|
||||||
// mariaDB may be nil if we're in offline mode
|
// mariaDB may be nil if we're in offline mode
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
@@ -672,7 +664,7 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := configService.Create(dbUserID, &req) // use DB user ID
|
config, err := configService.Create(dbUsername, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -746,7 +738,7 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := configService.CloneNoAuth(uuid, req.Name, dbUserID)
|
config, err := configService.CloneNoAuth(uuid, req.Name, dbUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/middleware"
|
"git.mchus.pro/mchus/quoteforge/internal/middleware"
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/services"
|
"git.mchus.pro/mchus/quoteforge/internal/services"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigurationHandler struct {
|
type ConfigurationHandler struct {
|
||||||
@@ -26,11 +25,11 @@ func NewConfigurationHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConfigurationHandler) List(c *gin.Context) {
|
func (h *ConfigurationHandler) List(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
||||||
|
|
||||||
configs, total, err := h.configService.ListByUser(userID, page, perPage)
|
configs, total, err := h.configService.ListByUser(username, page, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -45,7 +44,7 @@ func (h *ConfigurationHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConfigurationHandler) Create(c *gin.Context) {
|
func (h *ConfigurationHandler) Create(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
|
|
||||||
var req services.CreateConfigRequest
|
var req services.CreateConfigRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -53,7 +52,7 @@ func (h *ConfigurationHandler) Create(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := h.configService.Create(userID, &req)
|
config, err := h.configService.Create(username, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -63,10 +62,10 @@ func (h *ConfigurationHandler) Create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConfigurationHandler) Get(c *gin.Context) {
|
func (h *ConfigurationHandler) Get(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
|
|
||||||
config, err := h.configService.GetByUUID(uuid, userID)
|
config, err := h.configService.GetByUUID(uuid, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusNotFound
|
status := http.StatusNotFound
|
||||||
if err == services.ErrConfigForbidden {
|
if err == services.ErrConfigForbidden {
|
||||||
@@ -80,7 +79,7 @@ func (h *ConfigurationHandler) Get(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConfigurationHandler) Update(c *gin.Context) {
|
func (h *ConfigurationHandler) Update(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
|
|
||||||
var req services.CreateConfigRequest
|
var req services.CreateConfigRequest
|
||||||
@@ -89,7 +88,7 @@ func (h *ConfigurationHandler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := h.configService.Update(uuid, userID, &req)
|
config, err := h.configService.Update(uuid, username, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusInternalServerError
|
status := http.StatusInternalServerError
|
||||||
if err == services.ErrConfigNotFound {
|
if err == services.ErrConfigNotFound {
|
||||||
@@ -105,10 +104,10 @@ func (h *ConfigurationHandler) Update(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConfigurationHandler) Delete(c *gin.Context) {
|
func (h *ConfigurationHandler) Delete(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
|
|
||||||
err := h.configService.Delete(uuid, userID)
|
err := h.configService.Delete(uuid, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusInternalServerError
|
status := http.StatusInternalServerError
|
||||||
if err == services.ErrConfigNotFound {
|
if err == services.ErrConfigNotFound {
|
||||||
@@ -128,7 +127,7 @@ type RenameConfigRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConfigurationHandler) Rename(c *gin.Context) {
|
func (h *ConfigurationHandler) Rename(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
|
|
||||||
var req RenameConfigRequest
|
var req RenameConfigRequest
|
||||||
@@ -137,7 +136,7 @@ func (h *ConfigurationHandler) Rename(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := h.configService.Rename(uuid, userID, req.Name)
|
config, err := h.configService.Rename(uuid, username, req.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusInternalServerError
|
status := http.StatusInternalServerError
|
||||||
if err == services.ErrConfigNotFound {
|
if err == services.ErrConfigNotFound {
|
||||||
@@ -157,7 +156,7 @@ type CloneConfigRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConfigurationHandler) Clone(c *gin.Context) {
|
func (h *ConfigurationHandler) Clone(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
|
|
||||||
var req CloneConfigRequest
|
var req CloneConfigRequest
|
||||||
@@ -166,7 +165,7 @@ func (h *ConfigurationHandler) Clone(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := h.configService.Clone(uuid, userID, req.Name)
|
config, err := h.configService.Clone(uuid, username, req.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusInternalServerError
|
status := http.StatusInternalServerError
|
||||||
if err == services.ErrConfigNotFound {
|
if err == services.ErrConfigNotFound {
|
||||||
@@ -182,10 +181,10 @@ func (h *ConfigurationHandler) Clone(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ConfigurationHandler) RefreshPrices(c *gin.Context) {
|
func (h *ConfigurationHandler) RefreshPrices(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
|
|
||||||
config, err := h.configService.RefreshPrices(uuid, userID)
|
config, err := h.configService.RefreshPrices(uuid, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusInternalServerError
|
status := http.StatusInternalServerError
|
||||||
if err == services.ErrConfigNotFound {
|
if err == services.ErrConfigNotFound {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/middleware"
|
"git.mchus.pro/mchus/quoteforge/internal/middleware"
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/services"
|
"git.mchus.pro/mchus/quoteforge/internal/services"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExportHandler struct {
|
type ExportHandler struct {
|
||||||
@@ -98,10 +98,10 @@ func (h *ExportHandler) buildExportData(req *ExportRequest) *services.ExportData
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ExportHandler) ExportConfigCSV(c *gin.Context) {
|
func (h *ExportHandler) ExportConfigCSV(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
username := middleware.GetUsername(c)
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
|
|
||||||
config, err := h.configService.GetByUUID(uuid, userID)
|
config, err := h.configService.GetByUUID(uuid, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -18,19 +18,24 @@ func ConfigurationToLocal(cfg *models.Configuration) *LocalConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
local := &LocalConfiguration{
|
local := &LocalConfiguration{
|
||||||
UUID: cfg.UUID,
|
UUID: cfg.UUID,
|
||||||
Name: cfg.Name,
|
Name: cfg.Name,
|
||||||
Items: items,
|
Items: items,
|
||||||
TotalPrice: cfg.TotalPrice,
|
TotalPrice: cfg.TotalPrice,
|
||||||
CustomPrice: cfg.CustomPrice,
|
CustomPrice: cfg.CustomPrice,
|
||||||
Notes: cfg.Notes,
|
Notes: cfg.Notes,
|
||||||
IsTemplate: cfg.IsTemplate,
|
IsTemplate: cfg.IsTemplate,
|
||||||
ServerCount: cfg.ServerCount,
|
ServerCount: cfg.ServerCount,
|
||||||
PriceUpdatedAt: cfg.PriceUpdatedAt,
|
PriceUpdatedAt: cfg.PriceUpdatedAt,
|
||||||
CreatedAt: cfg.CreatedAt,
|
CreatedAt: cfg.CreatedAt,
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
SyncStatus: "pending",
|
SyncStatus: "pending",
|
||||||
OriginalUserID: cfg.UserID,
|
OriginalUserID: cfg.UserID,
|
||||||
|
OriginalUsername: cfg.OwnerUsername,
|
||||||
|
}
|
||||||
|
|
||||||
|
if local.OriginalUsername == "" && cfg.User != nil {
|
||||||
|
local.OriginalUsername = cfg.User.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.ID > 0 {
|
if cfg.ID > 0 {
|
||||||
@@ -55,6 +60,7 @@ func LocalToConfiguration(local *LocalConfiguration) *models.Configuration {
|
|||||||
cfg := &models.Configuration{
|
cfg := &models.Configuration{
|
||||||
UUID: local.UUID,
|
UUID: local.UUID,
|
||||||
UserID: local.OriginalUserID,
|
UserID: local.OriginalUserID,
|
||||||
|
OwnerUsername: local.OriginalUsername,
|
||||||
Name: local.Name,
|
Name: local.Name,
|
||||||
Items: items,
|
Items: items,
|
||||||
TotalPrice: local.TotalPrice,
|
TotalPrice: local.TotalPrice,
|
||||||
|
|||||||
@@ -59,22 +59,23 @@ func (c LocalConfigItems) Total() float64 {
|
|||||||
|
|
||||||
// LocalConfiguration stores configurations in local SQLite
|
// LocalConfiguration stores configurations in local SQLite
|
||||||
type LocalConfiguration struct {
|
type LocalConfiguration struct {
|
||||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
UUID string `gorm:"uniqueIndex;not null" json:"uuid"`
|
UUID string `gorm:"uniqueIndex;not null" json:"uuid"`
|
||||||
ServerID *uint `json:"server_id"` // ID on MariaDB server, NULL if local only
|
ServerID *uint `json:"server_id"` // ID on MariaDB server, NULL if local only
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Items LocalConfigItems `gorm:"type:text" json:"items"` // JSON stored as text in SQLite
|
Items LocalConfigItems `gorm:"type:text" json:"items"` // JSON stored as text in SQLite
|
||||||
TotalPrice *float64 `json:"total_price"`
|
TotalPrice *float64 `json:"total_price"`
|
||||||
CustomPrice *float64 `json:"custom_price"`
|
CustomPrice *float64 `json:"custom_price"`
|
||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
IsTemplate bool `gorm:"default:false" json:"is_template"`
|
IsTemplate bool `gorm:"default:false" json:"is_template"`
|
||||||
ServerCount int `gorm:"default:1" json:"server_count"`
|
ServerCount int `gorm:"default:1" json:"server_count"`
|
||||||
PriceUpdatedAt *time.Time `gorm:"type:timestamp" json:"price_updated_at,omitempty"`
|
PriceUpdatedAt *time.Time `gorm:"type:timestamp" json:"price_updated_at,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
SyncedAt *time.Time `json:"synced_at"`
|
SyncedAt *time.Time `json:"synced_at"`
|
||||||
SyncStatus string `gorm:"default:'local'" json:"sync_status"` // 'local', 'synced', 'modified'
|
SyncStatus string `gorm:"default:'local'" json:"sync_status"` // 'local', 'synced', 'modified'
|
||||||
OriginalUserID uint `json:"original_user_id"` // UserID from MariaDB for reference
|
OriginalUserID uint `json:"original_user_id"` // UserID from MariaDB for reference
|
||||||
|
OriginalUsername string `gorm:"not null;default:'';index" json:"original_username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (LocalConfiguration) TableName() string {
|
func (LocalConfiguration) TableName() string {
|
||||||
@@ -83,13 +84,13 @@ func (LocalConfiguration) TableName() string {
|
|||||||
|
|
||||||
// LocalPricelist stores cached pricelists from server
|
// LocalPricelist stores cached pricelists from server
|
||||||
type LocalPricelist struct {
|
type LocalPricelist struct {
|
||||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
ServerID uint `gorm:"not null" json:"server_id"` // ID on MariaDB server
|
ServerID uint `gorm:"not null" json:"server_id"` // ID on MariaDB server
|
||||||
Version string `gorm:"uniqueIndex;not null" json:"version"`
|
Version string `gorm:"uniqueIndex;not null" json:"version"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
SyncedAt time.Time `json:"synced_at"`
|
SyncedAt time.Time `json:"synced_at"`
|
||||||
IsUsed bool `gorm:"default:false" json:"is_used"` // Used by any local configuration
|
IsUsed bool `gorm:"default:false" json:"is_used"` // Used by any local configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (LocalPricelist) TableName() string {
|
func (LocalPricelist) TableName() string {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/services"
|
"git.mchus.pro/mchus/quoteforge/internal/services"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -99,3 +99,12 @@ func GetUserID(c *gin.Context) uint {
|
|||||||
}
|
}
|
||||||
return claims.UserID
|
return claims.UserID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsername extracts username from context
|
||||||
|
func GetUsername(c *gin.Context) string {
|
||||||
|
claims := GetClaims(c)
|
||||||
|
if claims == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return claims.Username
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,18 +40,19 @@ func (c ConfigItems) Total() float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
UUID string `gorm:"size:36;uniqueIndex;not null" json:"uuid"`
|
UUID string `gorm:"size:36;uniqueIndex;not null" json:"uuid"`
|
||||||
UserID uint `gorm:"not null" json:"user_id"`
|
UserID uint `gorm:"not null" json:"user_id"` // Legacy owner field (kept for backward compatibility)
|
||||||
Name string `gorm:"size:200;not null" json:"name"`
|
OwnerUsername string `gorm:"size:100;not null;default:'';index" json:"owner_username"`
|
||||||
Items ConfigItems `gorm:"type:json;not null" json:"items"`
|
Name string `gorm:"size:200;not null" json:"name"`
|
||||||
TotalPrice *float64 `gorm:"type:decimal(12,2)" json:"total_price"`
|
Items ConfigItems `gorm:"type:json;not null" json:"items"`
|
||||||
CustomPrice *float64 `gorm:"type:decimal(12,2)" json:"custom_price"`
|
TotalPrice *float64 `gorm:"type:decimal(12,2)" json:"total_price"`
|
||||||
Notes string `gorm:"type:text" json:"notes"`
|
CustomPrice *float64 `gorm:"type:decimal(12,2)" json:"custom_price"`
|
||||||
IsTemplate bool `gorm:"default:false" json:"is_template"`
|
Notes string `gorm:"type:text" json:"notes"`
|
||||||
ServerCount int `gorm:"default:1" json:"server_count"`
|
IsTemplate bool `gorm:"default:false" json:"is_template"`
|
||||||
PriceUpdatedAt *time.Time `gorm:"type:timestamp" json:"price_updated_at,omitempty"`
|
ServerCount int `gorm:"default:1" json:"server_count"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
PriceUpdatedAt *time.Time `gorm:"type:timestamp" json:"price_updated_at,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|
||||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,16 @@ func (r *ConfigurationRepository) Delete(id uint) error {
|
|||||||
return r.db.Delete(&models.Configuration{}, id).Error
|
return r.db.Delete(&models.Configuration{}, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConfigurationRepository) ListByUser(userID uint, offset, limit int) ([]models.Configuration, int64, error) {
|
func (r *ConfigurationRepository) ListByUser(ownerUsername string, offset, limit int) ([]models.Configuration, int64, error) {
|
||||||
var configs []models.Configuration
|
var configs []models.Configuration
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
r.db.Model(&models.Configuration{}).Where("user_id = ?", userID).Count(&total)
|
ownerScope := "owner_username = ? OR (COALESCE(owner_username, '') = '' AND user_id IN (SELECT id FROM qt_users WHERE username = ?))"
|
||||||
|
|
||||||
|
r.db.Model(&models.Configuration{}).Where(ownerScope, ownerUsername, ownerUsername).Count(&total)
|
||||||
err := r.db.
|
err := r.db.
|
||||||
Where("user_id = ?", userID).
|
Preload("User").
|
||||||
|
Where(ownerScope, ownerUsername, ownerUsername).
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
Offset(offset).
|
Offset(offset).
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
@@ -81,6 +84,7 @@ func (r *ConfigurationRepository) ListAll(offset, limit int) ([]models.Configura
|
|||||||
|
|
||||||
r.db.Model(&models.Configuration{}).Count(&total)
|
r.db.Model(&models.Configuration{}).Count(&total)
|
||||||
err := r.db.
|
err := r.db.
|
||||||
|
Preload("User").
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
Offset(offset).
|
Offset(offset).
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type DataSource interface {
|
|||||||
|
|
||||||
// Configurations
|
// Configurations
|
||||||
SaveConfiguration(cfg *models.Configuration) error
|
SaveConfiguration(cfg *models.Configuration) error
|
||||||
GetConfigurations(userID uint) ([]models.Configuration, error)
|
GetConfigurations(ownerUsername string) ([]models.Configuration, error)
|
||||||
GetConfigurationByUUID(uuid string) (*models.Configuration, error)
|
GetConfigurationByUUID(uuid string) (*models.Configuration, error)
|
||||||
DeleteConfiguration(uuid string) error
|
DeleteConfiguration(uuid string) error
|
||||||
|
|
||||||
@@ -159,16 +159,17 @@ func (r *UnifiedRepo) SaveConfiguration(cfg *models.Configuration) error {
|
|||||||
|
|
||||||
// Offline: save to local SQLite and queue for sync
|
// Offline: save to local SQLite and queue for sync
|
||||||
localCfg := &localdb.LocalConfiguration{
|
localCfg := &localdb.LocalConfiguration{
|
||||||
UUID: cfg.UUID,
|
UUID: cfg.UUID,
|
||||||
Name: cfg.Name,
|
Name: cfg.Name,
|
||||||
TotalPrice: cfg.TotalPrice,
|
TotalPrice: cfg.TotalPrice,
|
||||||
CustomPrice: cfg.CustomPrice,
|
CustomPrice: cfg.CustomPrice,
|
||||||
Notes: cfg.Notes,
|
Notes: cfg.Notes,
|
||||||
IsTemplate: cfg.IsTemplate,
|
IsTemplate: cfg.IsTemplate,
|
||||||
ServerCount: cfg.ServerCount,
|
ServerCount: cfg.ServerCount,
|
||||||
CreatedAt: cfg.CreatedAt,
|
CreatedAt: cfg.CreatedAt,
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
SyncStatus: "pending",
|
SyncStatus: "pending",
|
||||||
|
OriginalUsername: cfg.OwnerUsername,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert items
|
// Convert items
|
||||||
@@ -196,10 +197,10 @@ func (r *UnifiedRepo) SaveConfiguration(cfg *models.Configuration) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigurations returns all configurations for a user
|
// GetConfigurations returns all configurations for a user
|
||||||
func (r *UnifiedRepo) GetConfigurations(userID uint) ([]models.Configuration, error) {
|
func (r *UnifiedRepo) GetConfigurations(ownerUsername string) ([]models.Configuration, error) {
|
||||||
if r.isOnline {
|
if r.isOnline {
|
||||||
repo := NewConfigurationRepository(r.mariaDB)
|
repo := NewConfigurationRepository(r.mariaDB)
|
||||||
configs, _, err := repo.ListByUser(userID, 0, 1000)
|
configs, _, err := repo.ListByUser(ownerUsername, 0, 1000)
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,15 +223,16 @@ func (r *UnifiedRepo) GetConfigurations(userID uint) ([]models.Configuration, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
result[i] = models.Configuration{
|
result[i] = models.Configuration{
|
||||||
UUID: lc.UUID,
|
UUID: lc.UUID,
|
||||||
Name: lc.Name,
|
OwnerUsername: lc.OriginalUsername,
|
||||||
Items: items,
|
Name: lc.Name,
|
||||||
TotalPrice: lc.TotalPrice,
|
Items: items,
|
||||||
CustomPrice: lc.CustomPrice,
|
TotalPrice: lc.TotalPrice,
|
||||||
Notes: lc.Notes,
|
CustomPrice: lc.CustomPrice,
|
||||||
IsTemplate: lc.IsTemplate,
|
Notes: lc.Notes,
|
||||||
ServerCount: lc.ServerCount,
|
IsTemplate: lc.IsTemplate,
|
||||||
CreatedAt: lc.CreatedAt,
|
ServerCount: lc.ServerCount,
|
||||||
|
CreatedAt: lc.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrConfigNotFound = errors.New("configuration not found")
|
ErrConfigNotFound = errors.New("configuration not found")
|
||||||
ErrConfigForbidden = errors.New("access to configuration forbidden")
|
ErrConfigForbidden = errors.New("access to configuration forbidden")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigurationGetter is an interface for services that can retrieve configurations
|
// ConfigurationGetter is an interface for services that can retrieve configurations
|
||||||
// Used by handlers to work with both ConfigurationService and LocalConfigurationService
|
// Used by handlers to work with both ConfigurationService and LocalConfigurationService
|
||||||
type ConfigurationGetter interface {
|
type ConfigurationGetter interface {
|
||||||
GetByUUID(uuid string, userID uint) (*models.Configuration, error)
|
GetByUUID(uuid string, ownerUsername string) (*models.Configuration, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigurationService struct {
|
type ConfigurationService struct {
|
||||||
@@ -39,15 +39,15 @@ func NewConfigurationService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateConfigRequest struct {
|
type CreateConfigRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Items models.ConfigItems `json:"items"`
|
Items models.ConfigItems `json:"items"`
|
||||||
CustomPrice *float64 `json:"custom_price"`
|
CustomPrice *float64 `json:"custom_price"`
|
||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
IsTemplate bool `json:"is_template"`
|
IsTemplate bool `json:"is_template"`
|
||||||
ServerCount int `json:"server_count"`
|
ServerCount int `json:"server_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConfigurationService) Create(userID uint, req *CreateConfigRequest) (*models.Configuration, error) {
|
func (s *ConfigurationService) Create(ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) {
|
||||||
total := req.Items.Total()
|
total := req.Items.Total()
|
||||||
|
|
||||||
// If server count is greater than 1, multiply the total by server count
|
// If server count is greater than 1, multiply the total by server count
|
||||||
@@ -56,15 +56,15 @@ func (s *ConfigurationService) Create(userID uint, req *CreateConfigRequest) (*m
|
|||||||
}
|
}
|
||||||
|
|
||||||
config := &models.Configuration{
|
config := &models.Configuration{
|
||||||
UUID: uuid.New().String(),
|
UUID: uuid.New().String(),
|
||||||
UserID: userID,
|
OwnerUsername: ownerUsername,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Items: req.Items,
|
Items: req.Items,
|
||||||
TotalPrice: &total,
|
TotalPrice: &total,
|
||||||
CustomPrice: req.CustomPrice,
|
CustomPrice: req.CustomPrice,
|
||||||
Notes: req.Notes,
|
Notes: req.Notes,
|
||||||
IsTemplate: req.IsTemplate,
|
IsTemplate: req.IsTemplate,
|
||||||
ServerCount: req.ServerCount,
|
ServerCount: req.ServerCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.configRepo.Create(config); err != nil {
|
if err := s.configRepo.Create(config); err != nil {
|
||||||
@@ -77,27 +77,27 @@ func (s *ConfigurationService) Create(userID uint, req *CreateConfigRequest) (*m
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConfigurationService) GetByUUID(uuid string, userID uint) (*models.Configuration, error) {
|
func (s *ConfigurationService) GetByUUID(uuid string, ownerUsername string) (*models.Configuration, error) {
|
||||||
config, err := s.configRepo.GetByUUID(uuid)
|
config, err := s.configRepo.GetByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrConfigNotFound
|
return nil, ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow access if user owns config or it's a template
|
// Allow access if user owns config or it's a template
|
||||||
if config.UserID != userID && !config.IsTemplate {
|
if !s.isOwner(config, ownerUsername) && !config.IsTemplate {
|
||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConfigurationService) Update(uuid string, userID uint, req *CreateConfigRequest) (*models.Configuration, error) {
|
func (s *ConfigurationService) Update(uuid string, ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) {
|
||||||
config, err := s.configRepo.GetByUUID(uuid)
|
config, err := s.configRepo.GetByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrConfigNotFound
|
return nil, ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.UserID != userID {
|
if !s.isOwner(config, ownerUsername) {
|
||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,26 +123,26 @@ func (s *ConfigurationService) Update(uuid string, userID uint, req *CreateConfi
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConfigurationService) Delete(uuid string, userID uint) error {
|
func (s *ConfigurationService) Delete(uuid string, ownerUsername string) error {
|
||||||
config, err := s.configRepo.GetByUUID(uuid)
|
config, err := s.configRepo.GetByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrConfigNotFound
|
return ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.UserID != userID {
|
if !s.isOwner(config, ownerUsername) {
|
||||||
return ErrConfigForbidden
|
return ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.configRepo.Delete(config.ID)
|
return s.configRepo.Delete(config.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConfigurationService) Rename(uuid string, userID uint, newName string) (*models.Configuration, error) {
|
func (s *ConfigurationService) Rename(uuid string, ownerUsername string, newName string) (*models.Configuration, error) {
|
||||||
config, err := s.configRepo.GetByUUID(uuid)
|
config, err := s.configRepo.GetByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrConfigNotFound
|
return nil, ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.UserID != userID {
|
if !s.isOwner(config, ownerUsername) {
|
||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +155,8 @@ func (s *ConfigurationService) Rename(uuid string, userID uint, newName string)
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConfigurationService) Clone(configUUID string, userID uint, newName string) (*models.Configuration, error) {
|
func (s *ConfigurationService) Clone(configUUID string, ownerUsername string, newName string) (*models.Configuration, error) {
|
||||||
original, err := s.GetByUUID(configUUID, userID)
|
original, err := s.GetByUUID(configUUID, ownerUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -170,15 +170,15 @@ func (s *ConfigurationService) Clone(configUUID string, userID uint, newName str
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone := &models.Configuration{
|
clone := &models.Configuration{
|
||||||
UUID: uuid.New().String(),
|
UUID: uuid.New().String(),
|
||||||
UserID: userID,
|
OwnerUsername: ownerUsername,
|
||||||
Name: newName,
|
Name: newName,
|
||||||
Items: original.Items,
|
Items: original.Items,
|
||||||
TotalPrice: &total,
|
TotalPrice: &total,
|
||||||
CustomPrice: original.CustomPrice,
|
CustomPrice: original.CustomPrice,
|
||||||
Notes: original.Notes,
|
Notes: original.Notes,
|
||||||
IsTemplate: false, // Clone is never a template
|
IsTemplate: false, // Clone is never a template
|
||||||
ServerCount: original.ServerCount,
|
ServerCount: original.ServerCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.configRepo.Create(clone); err != nil {
|
if err := s.configRepo.Create(clone); err != nil {
|
||||||
@@ -188,7 +188,7 @@ func (s *ConfigurationService) Clone(configUUID string, userID uint, newName str
|
|||||||
return clone, nil
|
return clone, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConfigurationService) ListByUser(userID uint, page, perPage int) ([]models.Configuration, int64, error) {
|
func (s *ConfigurationService) ListByUser(ownerUsername string, page, perPage int) ([]models.Configuration, int64, error) {
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ func (s *ConfigurationService) ListByUser(userID uint, page, perPage int) ([]mod
|
|||||||
}
|
}
|
||||||
offset := (page - 1) * perPage
|
offset := (page - 1) * perPage
|
||||||
|
|
||||||
return s.configRepo.ListByUser(userID, offset, perPage)
|
return s.configRepo.ListByUser(ownerUsername, offset, perPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListAll returns all configurations without user filter (for use when auth is disabled)
|
// ListAll returns all configurations without user filter (for use when auth is disabled)
|
||||||
@@ -274,7 +274,7 @@ func (s *ConfigurationService) RenameNoAuth(uuid string, newName string) (*model
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CloneNoAuth clones configuration without ownership check
|
// CloneNoAuth clones configuration without ownership check
|
||||||
func (s *ConfigurationService) CloneNoAuth(configUUID string, newName string, userID uint) (*models.Configuration, error) {
|
func (s *ConfigurationService) CloneNoAuth(configUUID string, newName string, ownerUsername string) (*models.Configuration, error) {
|
||||||
original, err := s.configRepo.GetByUUID(configUUID)
|
original, err := s.configRepo.GetByUUID(configUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrConfigNotFound
|
return nil, ErrConfigNotFound
|
||||||
@@ -286,15 +286,15 @@ func (s *ConfigurationService) CloneNoAuth(configUUID string, newName string, us
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone := &models.Configuration{
|
clone := &models.Configuration{
|
||||||
UUID: uuid.New().String(),
|
UUID: uuid.New().String(),
|
||||||
UserID: userID, // Use provided user ID
|
OwnerUsername: ownerUsername,
|
||||||
Name: newName,
|
Name: newName,
|
||||||
Items: original.Items,
|
Items: original.Items,
|
||||||
TotalPrice: &total,
|
TotalPrice: &total,
|
||||||
CustomPrice: original.CustomPrice,
|
CustomPrice: original.CustomPrice,
|
||||||
Notes: original.Notes,
|
Notes: original.Notes,
|
||||||
IsTemplate: false,
|
IsTemplate: false,
|
||||||
ServerCount: original.ServerCount,
|
ServerCount: original.ServerCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.configRepo.Create(clone); err != nil {
|
if err := s.configRepo.Create(clone); err != nil {
|
||||||
@@ -356,13 +356,13 @@ func (s *ConfigurationService) ListTemplates(page, perPage int) ([]models.Config
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RefreshPrices updates all component prices in the configuration with current prices
|
// RefreshPrices updates all component prices in the configuration with current prices
|
||||||
func (s *ConfigurationService) RefreshPrices(uuid string, userID uint) (*models.Configuration, error) {
|
func (s *ConfigurationService) RefreshPrices(uuid string, ownerUsername string) (*models.Configuration, error) {
|
||||||
config, err := s.configRepo.GetByUUID(uuid)
|
config, err := s.configRepo.GetByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrConfigNotFound
|
return nil, ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.UserID != userID {
|
if !s.isOwner(config, ownerUsername) {
|
||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +407,19 @@ func (s *ConfigurationService) RefreshPrices(uuid string, userID uint) (*models.
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ConfigurationService) isOwner(config *models.Configuration, ownerUsername string) bool {
|
||||||
|
if config == nil || ownerUsername == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if config.OwnerUsername != "" {
|
||||||
|
return config.OwnerUsername == ownerUsername
|
||||||
|
}
|
||||||
|
if config.User != nil {
|
||||||
|
return config.User.Username == ownerUsername
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// // Export configuration as JSON
|
// // Export configuration as JSON
|
||||||
// type ConfigExport struct {
|
// type ConfigExport struct {
|
||||||
// Name string `json:"name"`
|
// Name string `json:"name"`
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func NewLocalConfigurationService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new configuration in local SQLite and queues it for sync
|
// Create creates a new configuration in local SQLite and queues it for sync
|
||||||
func (s *LocalConfigurationService) Create(userID uint, req *CreateConfigRequest) (*models.Configuration, error) {
|
func (s *LocalConfigurationService) Create(ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) {
|
||||||
// If online, check for new pricelists first
|
// If online, check for new pricelists first
|
||||||
if s.isOnline() {
|
if s.isOnline() {
|
||||||
if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
|
if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
|
||||||
@@ -49,16 +49,16 @@ func (s *LocalConfigurationService) Create(userID uint, req *CreateConfigRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := &models.Configuration{
|
cfg := &models.Configuration{
|
||||||
UUID: uuid.New().String(),
|
UUID: uuid.New().String(),
|
||||||
UserID: userID,
|
OwnerUsername: ownerUsername,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Items: req.Items,
|
Items: req.Items,
|
||||||
TotalPrice: &total,
|
TotalPrice: &total,
|
||||||
CustomPrice: req.CustomPrice,
|
CustomPrice: req.CustomPrice,
|
||||||
Notes: req.Notes,
|
Notes: req.Notes,
|
||||||
IsTemplate: req.IsTemplate,
|
IsTemplate: req.IsTemplate,
|
||||||
ServerCount: req.ServerCount,
|
ServerCount: req.ServerCount,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to local model
|
// Convert to local model
|
||||||
@@ -85,7 +85,7 @@ func (s *LocalConfigurationService) Create(userID uint, req *CreateConfigRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByUUID returns a configuration from local SQLite
|
// GetByUUID returns a configuration from local SQLite
|
||||||
func (s *LocalConfigurationService) GetByUUID(uuid string, userID uint) (*models.Configuration, error) {
|
func (s *LocalConfigurationService) GetByUUID(uuid string, ownerUsername string) (*models.Configuration, error) {
|
||||||
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrConfigNotFound
|
return nil, ErrConfigNotFound
|
||||||
@@ -95,7 +95,7 @@ func (s *LocalConfigurationService) GetByUUID(uuid string, userID uint) (*models
|
|||||||
cfg := localdb.LocalToConfiguration(localCfg)
|
cfg := localdb.LocalToConfiguration(localCfg)
|
||||||
|
|
||||||
// Allow access if user owns config or it's a template
|
// Allow access if user owns config or it's a template
|
||||||
if cfg.UserID != userID && !cfg.IsTemplate {
|
if !s.isOwner(localCfg, ownerUsername) && !cfg.IsTemplate {
|
||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,13 +103,13 @@ func (s *LocalConfigurationService) GetByUUID(uuid string, userID uint) (*models
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update updates a configuration in local SQLite and queues it for sync
|
// Update updates a configuration in local SQLite and queues it for sync
|
||||||
func (s *LocalConfigurationService) Update(uuid string, userID uint, req *CreateConfigRequest) (*models.Configuration, error) {
|
func (s *LocalConfigurationService) Update(uuid string, ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) {
|
||||||
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrConfigNotFound
|
return nil, ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if localCfg.OriginalUserID != userID {
|
if !s.isOwner(localCfg, ownerUsername) {
|
||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,13 +155,13 @@ func (s *LocalConfigurationService) Update(uuid string, userID uint, req *Create
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes a configuration from local SQLite and queues it for sync
|
// Delete deletes a configuration from local SQLite and queues it for sync
|
||||||
func (s *LocalConfigurationService) Delete(uuid string, userID uint) error {
|
func (s *LocalConfigurationService) Delete(uuid string, ownerUsername string) error {
|
||||||
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrConfigNotFound
|
return ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if localCfg.OriginalUserID != userID {
|
if !s.isOwner(localCfg, ownerUsername) {
|
||||||
return ErrConfigForbidden
|
return ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,13 +179,13 @@ func (s *LocalConfigurationService) Delete(uuid string, userID uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rename renames a configuration
|
// Rename renames a configuration
|
||||||
func (s *LocalConfigurationService) Rename(uuid string, userID uint, newName string) (*models.Configuration, error) {
|
func (s *LocalConfigurationService) Rename(uuid string, ownerUsername string, newName string) (*models.Configuration, error) {
|
||||||
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrConfigNotFound
|
return nil, ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if localCfg.OriginalUserID != userID {
|
if !s.isOwner(localCfg, ownerUsername) {
|
||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,8 +211,8 @@ func (s *LocalConfigurationService) Rename(uuid string, userID uint, newName str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clone clones a configuration
|
// Clone clones a configuration
|
||||||
func (s *LocalConfigurationService) Clone(configUUID string, userID uint, newName string) (*models.Configuration, error) {
|
func (s *LocalConfigurationService) Clone(configUUID string, ownerUsername string, newName string) (*models.Configuration, error) {
|
||||||
original, err := s.GetByUUID(configUUID, userID)
|
original, err := s.GetByUUID(configUUID, ownerUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -223,16 +223,16 @@ func (s *LocalConfigurationService) Clone(configUUID string, userID uint, newNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone := &models.Configuration{
|
clone := &models.Configuration{
|
||||||
UUID: uuid.New().String(),
|
UUID: uuid.New().String(),
|
||||||
UserID: userID,
|
OwnerUsername: ownerUsername,
|
||||||
Name: newName,
|
Name: newName,
|
||||||
Items: original.Items,
|
Items: original.Items,
|
||||||
TotalPrice: &total,
|
TotalPrice: &total,
|
||||||
CustomPrice: original.CustomPrice,
|
CustomPrice: original.CustomPrice,
|
||||||
Notes: original.Notes,
|
Notes: original.Notes,
|
||||||
IsTemplate: false,
|
IsTemplate: false,
|
||||||
ServerCount: original.ServerCount,
|
ServerCount: original.ServerCount,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
localCfg := localdb.ConfigurationToLocal(clone)
|
localCfg := localdb.ConfigurationToLocal(clone)
|
||||||
@@ -253,7 +253,7 @@ func (s *LocalConfigurationService) Clone(configUUID string, userID uint, newNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListByUser returns all configurations for a user from local SQLite
|
// ListByUser returns all configurations for a user from local SQLite
|
||||||
func (s *LocalConfigurationService) ListByUser(userID uint, page, perPage int) ([]models.Configuration, int64, error) {
|
func (s *LocalConfigurationService) ListByUser(ownerUsername string, page, perPage int) ([]models.Configuration, int64, error) {
|
||||||
// Get all local configurations
|
// Get all local configurations
|
||||||
localConfigs, err := s.localDB.GetConfigurations()
|
localConfigs, err := s.localDB.GetConfigurations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -263,7 +263,7 @@ func (s *LocalConfigurationService) ListByUser(userID uint, page, perPage int) (
|
|||||||
// Filter by user
|
// Filter by user
|
||||||
var userConfigs []models.Configuration
|
var userConfigs []models.Configuration
|
||||||
for _, lc := range localConfigs {
|
for _, lc := range localConfigs {
|
||||||
if lc.OriginalUserID == userID || lc.IsTemplate {
|
if (lc.OriginalUsername == ownerUsername) || lc.IsTemplate {
|
||||||
userConfigs = append(userConfigs, *localdb.LocalToConfiguration(&lc))
|
userConfigs = append(userConfigs, *localdb.LocalToConfiguration(&lc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@ func (s *LocalConfigurationService) ListByUser(userID uint, page, perPage int) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RefreshPrices updates all component prices in the configuration from local cache
|
// RefreshPrices updates all component prices in the configuration from local cache
|
||||||
func (s *LocalConfigurationService) RefreshPrices(uuid string, userID uint) (*models.Configuration, error) {
|
func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername string) (*models.Configuration, error) {
|
||||||
// Get configuration from local SQLite
|
// Get configuration from local SQLite
|
||||||
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -300,7 +300,7 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, userID uint) (*mo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check ownership
|
// Check ownership
|
||||||
if localCfg.OriginalUserID != userID {
|
if !s.isOwner(localCfg, ownerUsername) {
|
||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +448,7 @@ func (s *LocalConfigurationService) RenameNoAuth(uuid string, newName string) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CloneNoAuth clones configuration without ownership check
|
// CloneNoAuth clones configuration without ownership check
|
||||||
func (s *LocalConfigurationService) CloneNoAuth(configUUID string, newName string, userID uint) (*models.Configuration, error) {
|
func (s *LocalConfigurationService) CloneNoAuth(configUUID string, newName string, ownerUsername string) (*models.Configuration, error) {
|
||||||
original, err := s.GetByUUIDNoAuth(configUUID)
|
original, err := s.GetByUUIDNoAuth(configUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -460,16 +460,16 @@ func (s *LocalConfigurationService) CloneNoAuth(configUUID string, newName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone := &models.Configuration{
|
clone := &models.Configuration{
|
||||||
UUID: uuid.New().String(),
|
UUID: uuid.New().String(),
|
||||||
UserID: userID,
|
OwnerUsername: ownerUsername,
|
||||||
Name: newName,
|
Name: newName,
|
||||||
Items: original.Items,
|
Items: original.Items,
|
||||||
TotalPrice: &total,
|
TotalPrice: &total,
|
||||||
CustomPrice: original.CustomPrice,
|
CustomPrice: original.CustomPrice,
|
||||||
Notes: original.Notes,
|
Notes: original.Notes,
|
||||||
IsTemplate: false,
|
IsTemplate: false,
|
||||||
ServerCount: original.ServerCount,
|
ServerCount: original.ServerCount,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
localCfg := localdb.ConfigurationToLocal(clone)
|
localCfg := localdb.ConfigurationToLocal(clone)
|
||||||
@@ -626,3 +626,13 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co
|
|||||||
func (s *LocalConfigurationService) ImportFromServer() (*sync.ConfigImportResult, error) {
|
func (s *LocalConfigurationService) ImportFromServer() (*sync.ConfigImportResult, error) {
|
||||||
return s.syncService.ImportConfigurationsToLocal()
|
return s.syncService.ImportConfigurationsToLocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *LocalConfigurationService) isOwner(cfg *localdb.LocalConfiguration, ownerUsername string) bool {
|
||||||
|
if cfg == nil || ownerUsername == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if cfg.OriginalUsername != "" {
|
||||||
|
return cfg.OriginalUsername == ownerUsername
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
10
migrations/005_add_owner_username.sql
Normal file
10
migrations/005_add_owner_username.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- Store configuration owner as username (instead of relying on numeric user_id)
|
||||||
|
ALTER TABLE qt_configurations
|
||||||
|
ADD COLUMN owner_username VARCHAR(100) NOT NULL DEFAULT '' AFTER user_id,
|
||||||
|
ADD INDEX idx_qt_configurations_owner_username (owner_username);
|
||||||
|
|
||||||
|
-- Backfill owner_username from qt_users for existing rows
|
||||||
|
UPDATE qt_configurations c
|
||||||
|
LEFT JOIN qt_users u ON u.id = c.user_id
|
||||||
|
SET c.owner_username = COALESCE(u.username, c.owner_username)
|
||||||
|
WHERE c.owner_username = '';
|
||||||
@@ -861,7 +861,7 @@ function renderAllConfigs(configs) {
|
|||||||
const date = new Date(c.created_at).toLocaleDateString('ru-RU');
|
const date = new Date(c.created_at).toLocaleDateString('ru-RU');
|
||||||
const total = c.total_price ? '$' + c.total_price.toLocaleString('en-US', {minimumFractionDigits: 2}) : '—';
|
const total = c.total_price ? '$' + c.total_price.toLocaleString('en-US', {minimumFractionDigits: 2}) : '—';
|
||||||
const serverCount = c.server_count ? c.server_count : 1;
|
const serverCount = c.server_count ? c.server_count : 1;
|
||||||
const username = c.user ? c.user.username : '—';
|
const username = c.owner_username || (c.user ? c.user.username : '—');
|
||||||
|
|
||||||
html += '<tr class="hover:bg-gray-50">';
|
html += '<tr class="hover:bg-gray-50">';
|
||||||
html += '<td class="px-3 py-2 text-sm text-gray-500">' + date + '</td>';
|
html += '<td class="px-3 py-2 text-sm text-gray-500">' + date + '</td>';
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ function renderConfigs(configs) {
|
|||||||
html += '<thead class="bg-gray-50"><tr>';
|
html += '<thead class="bg-gray-50"><tr>';
|
||||||
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Дата</th>';
|
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Дата</th>';
|
||||||
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Название</th>';
|
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Название</th>';
|
||||||
|
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Автор</th>';
|
||||||
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Цена (за 1 шт)</th>';
|
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Цена (за 1 шт)</th>';
|
||||||
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Кол-во</th>';
|
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Кол-во</th>';
|
||||||
html += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Сумма</th>';
|
html += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Сумма</th>';
|
||||||
@@ -137,6 +138,7 @@ function renderConfigs(configs) {
|
|||||||
const date = new Date(c.created_at).toLocaleDateString('ru-RU');
|
const date = new Date(c.created_at).toLocaleDateString('ru-RU');
|
||||||
const total = c.total_price ? '$' + c.total_price.toLocaleString('en-US', {minimumFractionDigits: 2}) : '—';
|
const total = c.total_price ? '$' + c.total_price.toLocaleString('en-US', {minimumFractionDigits: 2}) : '—';
|
||||||
const serverCount = c.server_count ? c.server_count : 1;
|
const serverCount = c.server_count ? c.server_count : 1;
|
||||||
|
const author = c.owner_username || (c.user && c.user.username) || '—';
|
||||||
|
|
||||||
// Calculate price per unit (total / server count)
|
// Calculate price per unit (total / server count)
|
||||||
let pricePerUnit = '—';
|
let pricePerUnit = '—';
|
||||||
@@ -148,6 +150,7 @@ function renderConfigs(configs) {
|
|||||||
html += '<tr class="hover:bg-gray-50">';
|
html += '<tr class="hover:bg-gray-50">';
|
||||||
html += '<td class="px-4 py-3 text-sm text-gray-500">' + date + '</td>';
|
html += '<td class="px-4 py-3 text-sm text-gray-500">' + date + '</td>';
|
||||||
html += '<td class="px-4 py-3 text-sm font-medium"><a href="/configurator?uuid=' + c.uuid + '" class="text-blue-600 hover:text-blue-800 hover:underline">' + escapeHtml(c.name) + '</a></td>';
|
html += '<td class="px-4 py-3 text-sm font-medium"><a href="/configurator?uuid=' + c.uuid + '" class="text-blue-600 hover:text-blue-800 hover:underline">' + escapeHtml(c.name) + '</a></td>';
|
||||||
|
html += '<td class="px-4 py-3 text-sm text-gray-500">' + escapeHtml(author) + '</td>';
|
||||||
html += '<td class="px-4 py-3 text-sm text-gray-500">' + pricePerUnit + '</td>';
|
html += '<td class="px-4 py-3 text-sm text-gray-500">' + pricePerUnit + '</td>';
|
||||||
html += '<td class="px-4 py-3 text-sm text-gray-500">' + serverCount + '</td>';
|
html += '<td class="px-4 py-3 text-sm text-gray-500">' + serverCount + '</td>';
|
||||||
html += '<td class="px-4 py-3 text-sm text-right">' + total + '</td>';
|
html += '<td class="px-4 py-3 text-sm text-right">' + total + '</td>';
|
||||||
|
|||||||
Reference in New Issue
Block a user