2026-07-16 01:47:11 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"floorvisualizer/internal/middleware"
|
|
|
|
|
"floorvisualizer/internal/model"
|
|
|
|
|
"floorvisualizer/internal/repository"
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-16 02:29:11 +00:00
|
|
|
func Me(db *sql.DB) http.HandlerFunc {
|
2026-07-16 01:47:11 +00:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
userID := middleware.UserID(r.Context())
|
|
|
|
|
switch r.Method {
|
|
|
|
|
case http.MethodGet:
|
|
|
|
|
user, err := repository.GetUserByID(db, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 404, map[string]string{"error": "user not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, user)
|
|
|
|
|
case http.MethodPut:
|
|
|
|
|
var input struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
AvatarURL string `json:"avatar_url"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
user, err := repository.UpdateUser(db, userID, input.Name, input.AvatarURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, user)
|
|
|
|
|
default:
|
|
|
|
|
writeErr(w, 405, "Method not allowed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 02:29:11 +00:00
|
|
|
func Favorites(db *sql.DB) http.HandlerFunc {
|
2026-07-16 01:47:11 +00:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
userID := middleware.UserID(r.Context())
|
|
|
|
|
switch r.Method {
|
|
|
|
|
case http.MethodGet:
|
|
|
|
|
favs, err := repository.ListFavorites(db, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if favs == nil {
|
|
|
|
|
favs = []model.Favorite{}
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, map[string]interface{}{"favorites": favs, "total": len(favs)})
|
|
|
|
|
case http.MethodPost:
|
|
|
|
|
sku := r.URL.Query().Get("sku")
|
|
|
|
|
if sku == "" || strings.Contains(sku, "/") {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "sku is required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
product, err := repository.GetProductBySKU(db, sku)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 404, map[string]string{"error": "product not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fav := model.Favorite{
|
|
|
|
|
SKU: product.SKU, Brand: product.Brand, SeriesName: product.SeriesName,
|
|
|
|
|
StyleName: product.StyleName, MainImageURL: product.MainImageURL,
|
|
|
|
|
Category: product.Category, Material: product.Material,
|
|
|
|
|
ColorTone: product.ColorTone, PricePerSqft: product.PricePerSqft,
|
|
|
|
|
}
|
|
|
|
|
if err := repository.AddFavorite(db, userID, fav); err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 201, fav)
|
|
|
|
|
case http.MethodDelete:
|
|
|
|
|
sku := strings.TrimSpace(strings.TrimPrefix(r.URL.Path, "/user/favorites/"))
|
|
|
|
|
if sku == "" || strings.Contains(sku, "/") {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "missing sku"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := repository.RemoveFavorite(db, userID, sku); err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, map[string]string{"ok": "removed"})
|
|
|
|
|
default:
|
|
|
|
|
writeErr(w, 405, "Method not allowed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 02:29:11 +00:00
|
|
|
func Projects(db *sql.DB) http.HandlerFunc {
|
2026-07-16 01:47:11 +00:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
userID := middleware.UserID(r.Context())
|
|
|
|
|
projectID := strings.TrimPrefix(r.URL.Path, "/user/projects/")
|
|
|
|
|
if projectID != "" && projectID != r.URL.Path {
|
|
|
|
|
handleSingleProject(w, r, db, userID, projectID)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
switch r.Method {
|
|
|
|
|
case http.MethodGet:
|
|
|
|
|
projects, err := repository.ListProjects(db, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if projects == nil {
|
|
|
|
|
projects = []model.Project{}
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, map[string]interface{}{"projects": projects, "total": len(projects)})
|
|
|
|
|
case http.MethodPost:
|
|
|
|
|
var input struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
OriginalImageURL string `json:"original_image_url"`
|
|
|
|
|
GeneratedImageURL string `json:"generated_image_url"`
|
|
|
|
|
FloorStyle string `json:"floor_style"`
|
|
|
|
|
Pattern string `json:"pattern"`
|
|
|
|
|
RoomType string `json:"room_type"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if input.Name == "" {
|
|
|
|
|
input.Name = "Untitled"
|
|
|
|
|
}
|
|
|
|
|
project, err := repository.CreateProject(db, userID, input.Name,
|
|
|
|
|
input.OriginalImageURL, input.GeneratedImageURL, input.FloorStyle, input.Pattern, input.RoomType)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 201, project)
|
|
|
|
|
default:
|
|
|
|
|
writeErr(w, 405, "Method not allowed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleSingleProject(w http.ResponseWriter, r *http.Request, db *sql.DB, userID, projectID string) {
|
|
|
|
|
switch r.Method {
|
|
|
|
|
case http.MethodGet:
|
|
|
|
|
project, err := repository.GetProject(db, projectID, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 404, map[string]string{"error": "project not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, project)
|
|
|
|
|
case http.MethodPut:
|
2026-07-16 02:29:11 +00:00
|
|
|
var input struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
2026-07-16 01:47:11 +00:00
|
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
project, err := repository.UpdateProject(db, projectID, userID, input.Name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, project)
|
|
|
|
|
case http.MethodDelete:
|
|
|
|
|
if err := repository.DeleteProject(db, projectID, userID); err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, map[string]string{"ok": "deleted"})
|
|
|
|
|
default:
|
|
|
|
|
writeErr(w, 405, "Method not allowed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 02:29:11 +00:00
|
|
|
func UploadAvatar(db *sql.DB) http.HandlerFunc {
|
2026-07-16 01:47:11 +00:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
userID := middleware.UserID(r.Context())
|
|
|
|
|
if err := r.ParseMultipartForm(5 << 20); err != nil {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "image too large (max 5MB)"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
file, _, err := r.FormFile("image")
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "missing image file"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
data, err := io.ReadAll(file)
|
|
|
|
|
if err != nil || len(data) == 0 {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "failed to read image"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ct := http.DetectContentType(data)
|
|
|
|
|
if !strings.HasPrefix(ct, "image/") {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "file is not an image"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hash := sha256.Sum256(data)
|
|
|
|
|
name := hex.EncodeToString(hash[:])[:16] + "_" + fmt.Sprintf("%d", time.Now().UnixNano())
|
|
|
|
|
dir := "uploads/avatars"
|
|
|
|
|
os.MkdirAll(dir, 0755)
|
|
|
|
|
filePath := filepath.Join(dir, name+".jpg")
|
|
|
|
|
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": "failed to save image"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current, err := repository.GetUserByID(db, userID)
|
|
|
|
|
if err != nil || current == nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": "user not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
avatarURL := "/uploads/avatars/" + name + ".jpg"
|
|
|
|
|
user, err := repository.UpdateUser(db, userID, current.Name, avatarURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, user)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Brand view tracking ───────────────────────────────────
|
|
|
|
|
|
2026-07-16 02:29:11 +00:00
|
|
|
func BrandView(db *sql.DB) http.HandlerFunc {
|
2026-07-16 01:47:11 +00:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
|
writeErr(w, 405, "Method not allowed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var body struct {
|
|
|
|
|
Brand string `json:"brand"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if body.Brand == "" {
|
|
|
|
|
writeJSON(w, 400, map[string]string{"error": "brand is required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userID := middleware.UserID(r.Context())
|
|
|
|
|
if userID == "" {
|
|
|
|
|
writeJSON(w, 401, map[string]string{"error": "login required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := repository.RecordBrandView(db, userID, body.Brand); err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, map[string]string{"ok": "recorded"})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 02:29:11 +00:00
|
|
|
func FrequentBrands(db *sql.DB) http.HandlerFunc {
|
2026-07-16 01:47:11 +00:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
userID := middleware.UserID(r.Context())
|
|
|
|
|
if userID == "" {
|
|
|
|
|
writeJSON(w, 401, map[string]string{"error": "login required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
brands, err := repository.GetFrequentBrands(db, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeJSON(w, 500, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if brands == nil {
|
|
|
|
|
brands = []repository.FrequentBrand{}
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, 200, map[string]interface{}{
|
|
|
|
|
"brands": brands,
|
|
|
|
|
"total": len(brands),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|