package handlers import ( "encoding/json" "fmt" "log/slog" "net/http" "git.nakama.town/fmartingr/hako/internal/dependencies" "git.nakama.town/fmartingr/hako/internal/model" "git.nakama.town/fmartingr/hako/internal/server/webcontext" ) // LinkHandler handles link-related HTTP requests type LinkHandler struct { deps model.Dependencies logger *slog.Logger } // NewLinkHandler creates a new LinkHandler func NewLinkHandler(deps model.Dependencies, logger *slog.Logger) *LinkHandler { return &LinkHandler{ deps: deps, logger: logger, } } // getDeps returns the concrete dependencies implementation func (h *LinkHandler) getDeps() *dependencies.Dependencies { return h.deps.(*dependencies.Dependencies) } // CreateLinkRequest represents a request to create a link type CreateLinkRequest struct { URL string `json:"url"` } // ListLinksResponse represents a paginated list of links type ListLinksResponse struct { Items []model.LinkListItem `json:"items"` Total int `json:"total"` Limit int `json:"limit"` Offset int `json:"offset"` } // HandleCreateLink handles POST /api/v1/links func (h *LinkHandler) HandleCreateLink(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } c := webcontext.NewWebContext(w, r) // Get user ID from context userID := c.GetUserID() if userID == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Parse request body var req CreateLinkRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Validate URL if req.URL == "" { http.Error(w, "URL is required", http.StatusBadRequest) return } // Create link link, err := h.deps.Domains().Links().CreateLink(c.Context(), req.URL, userID) if err != nil { h.logger.Error("Failed to create link", "error", err, "url", req.URL, "user_id", userID) http.Error(w, fmt.Sprintf("Failed to create link: %v", err), http.StatusInternalServerError) return } h.logger.Info("Link created successfully", "link_id", link.ID, "url", req.URL, "user_id", userID) // Build response using LinkListItem response := model.LinkListItem{ Link: *link, } c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusCreated) _ = json.NewEncoder(c.ResponseWriter()).Encode(response) } // HandleGetLink handles GET /api/v1/links/{id} func (h *LinkHandler) HandleGetLink(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } c := webcontext.NewWebContext(w, r) // Extract ID from path id := r.PathValue("id") if id == "" { http.Error(w, "Link ID is required", http.StatusBadRequest) return } // Get link link, err := h.deps.Domains().Links().GetLink(c.Context(), id) if err != nil { http.Error(w, "Link not found", http.StatusNotFound) return } // Build response using LinkListItem response := model.LinkListItem{ Link: model.Link{ ID: link.ID, URL: link.URL, UserID: link.UserID, TotalSize: link.TotalSize, CreatedAt: link.CreatedAt, UpdatedAt: link.UpdatedAt, }, } // Get latest archive for this link deps := h.getDeps() latestArchive, err := deps.ArchiveStore.GetLatestByLinkID(c.Context(), link.ID) if err == nil && latestArchive != nil { response.LatestArchiveTitle = latestArchive.Title response.LatestArchiveStatus = string(latestArchive.Status) // Get thumbnail using new query method thumbnailFile, err := deps.ArchiveFileStore.GetThumbnailByArchiveID(c.Context(), latestArchive.ID) if err == nil && thumbnailFile != nil { // Convert to ArchiveFileResponse response.Thumbnail = &model.ArchiveFileResponse{ ID: thumbnailFile.ID, ArchiveID: thumbnailFile.ArchiveID, ArchiverKey: thumbnailFile.ArchiverKey, Filename: thumbnailFile.Filename, MimeType: thumbnailFile.MimeType, FileSize: thumbnailFile.FileSize, HashSha256: thumbnailFile.HashSha256, CreatedAt: thumbnailFile.CreatedAt.Format("2006-01-02T15:04:05Z"), DownloadURL: fmt.Sprintf("/api/v1/archives/%s/files/%s/download", latestArchive.ID, thumbnailFile.ID), Content: thumbnailFile.Content, ContentMimeType: thumbnailFile.ContentMimeType, } } } // Get categories for this link categories, err := deps.LinkCategoryStore.GetCategoriesByLinkID(c.Context(), link.ID) if err == nil { response.Categories = make([]model.CategoryResponse, 0, len(categories)) for _, cat := range categories { response.Categories = append(response.Categories, model.CategoryResponse{ ID: cat.ID, Name: cat.Name, Icon: cat.Icon, }) } } c.ResponseWriter().Header().Set("Content-Type", "application/json") _ = json.NewEncoder(c.ResponseWriter()).Encode(response) } // HandleListLinks handles GET /api/v1/links func (h *LinkHandler) HandleListLinks(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } c := webcontext.NewWebContext(w, r) // Get user ID from context userID := c.GetUserID() if userID == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Parse request from URL query parameters req := model.LinkListRequest{UserID: userID} if err := req.FromURLValues(r.URL.Query()); err != nil { http.Error(w, fmt.Sprintf("Invalid query parameters: %v", err), http.StatusBadRequest) return } req.Defaults() if err := req.IsValid(); err != nil { http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest) return } // Debug: log search query if present if req.SearchQuery != "" { h.logger.Info("Search query received", "query", req.SearchQuery, "userID", userID) } // List links (domain layer handles thumbnail lookup) linkItems, total, err := h.deps.Domains().Links().ListLinks(c.Context(), req) if err != nil { http.Error(w, fmt.Sprintf("Failed to list links: %v", err), http.StatusInternalServerError) return } // Build response from domain structs - populate Thumbnail if available deps := h.getDeps() for i := range linkItems { item := &linkItems[i] // Get thumbnail file if available if item.HasThumbnail { thumbnailFile, err := deps.ArchiveFileStore.GetThumbnailByLinkID(c.Context(), item.ID) if err == nil && thumbnailFile != nil { // Get the archive ID for the thumbnail latestArchive, err := deps.ArchiveStore.GetLatestByLinkID(c.Context(), item.ID) if err == nil && latestArchive != nil { // Convert to ArchiveFileResponse item.Thumbnail = &model.ArchiveFileResponse{ ID: thumbnailFile.ID, ArchiveID: thumbnailFile.ArchiveID, ArchiverKey: thumbnailFile.ArchiverKey, Filename: thumbnailFile.Filename, MimeType: thumbnailFile.MimeType, FileSize: thumbnailFile.FileSize, HashSha256: thumbnailFile.HashSha256, CreatedAt: thumbnailFile.CreatedAt.Format("2006-01-02T15:04:05Z"), DownloadURL: fmt.Sprintf("/api/v1/archives/%s/files/%s/download", latestArchive.ID, thumbnailFile.ID), Content: thumbnailFile.Content, ContentMimeType: thumbnailFile.ContentMimeType, } } } } } response := ListLinksResponse{ Items: linkItems, Total: total, Limit: req.Limit, Offset: req.Offset, } c.ResponseWriter().Header().Set("Content-Type", "application/json") _ = json.NewEncoder(c.ResponseWriter()).Encode(response) } // HandleDeleteLink handles DELETE /api/v1/links/{id} func (h *LinkHandler) HandleDeleteLink(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } c := webcontext.NewWebContext(w, r) // Extract ID from path id := r.PathValue("id") if id == "" { http.Error(w, "Link ID is required", http.StatusBadRequest) return } // Delete link if err := h.deps.Domains().Links().DeleteLink(c.Context(), id); err != nil { http.Error(w, fmt.Sprintf("Failed to delete link: %v", err), http.StatusInternalServerError) return } c.ResponseWriter().WriteHeader(http.StatusNoContent) }