255 lines
8.1 KiB
Go
255 lines
8.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"atcr.io/pkg/appview/db"
|
|
"atcr.io/pkg/appview/middleware"
|
|
"atcr.io/pkg/atproto"
|
|
"atcr.io/pkg/auth/oauth"
|
|
"github.com/bluesky-social/indigo/atproto/identity"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// StarRepositoryHandler handles starring a repository
|
|
type StarRepositoryHandler struct {
|
|
DB *sql.DB
|
|
Directory identity.Directory
|
|
Refresher *oauth.Refresher
|
|
}
|
|
|
|
func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Get authenticated user from middleware
|
|
user := middleware.GetUser(r)
|
|
if user == nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Extract parameters
|
|
handle := chi.URLParam(r, "handle")
|
|
repository := chi.URLParam(r, "repository")
|
|
|
|
// Resolve owner's handle to DID
|
|
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
|
|
if err != nil {
|
|
slog.Warn("Failed to resolve handle for star", "handle", handle, "error", err)
|
|
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get OAuth session for the authenticated user
|
|
slog.Debug("Getting OAuth session for star", "user_did", user.DID)
|
|
session, err := h.Refresher.GetSession(r.Context(), user.DID)
|
|
if err != nil {
|
|
slog.Warn("Failed to get OAuth session for star", "user_did", user.DID, "error", err)
|
|
http.Error(w, fmt.Sprintf("Failed to get OAuth session: %v", err), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Get user's PDS client (use indigo's API client which handles DPoP automatically)
|
|
apiClient := session.APIClient()
|
|
pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient)
|
|
|
|
// Create star record
|
|
starRecord := atproto.NewStarRecord(ownerDID, repository)
|
|
rkey := atproto.StarRecordKey(ownerDID, repository)
|
|
|
|
// Write star record to user's PDS
|
|
_, err = pdsClient.PutRecord(r.Context(), atproto.StarCollection, rkey, starRecord)
|
|
if err != nil {
|
|
slog.Error("Failed to create star record", "error", err)
|
|
http.Error(w, fmt.Sprintf("Failed to create star: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Return success
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(map[string]bool{"starred": true})
|
|
}
|
|
|
|
// UnstarRepositoryHandler handles unstarring a repository
|
|
type UnstarRepositoryHandler struct {
|
|
DB *sql.DB
|
|
Directory identity.Directory
|
|
Refresher *oauth.Refresher
|
|
}
|
|
|
|
func (h *UnstarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Get authenticated user from middleware
|
|
user := middleware.GetUser(r)
|
|
if user == nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Extract parameters
|
|
handle := chi.URLParam(r, "handle")
|
|
repository := chi.URLParam(r, "repository")
|
|
|
|
// Resolve owner's handle to DID
|
|
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
|
|
if err != nil {
|
|
slog.Warn("Failed to resolve handle for unstar", "handle", handle, "error", err)
|
|
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get OAuth session for the authenticated user
|
|
slog.Debug("Getting OAuth session for unstar", "user_did", user.DID)
|
|
session, err := h.Refresher.GetSession(r.Context(), user.DID)
|
|
if err != nil {
|
|
slog.Warn("Failed to get OAuth session for unstar", "user_did", user.DID, "error", err)
|
|
http.Error(w, fmt.Sprintf("Failed to get OAuth session: %v", err), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Get user's PDS client (use indigo's API client which handles DPoP automatically)
|
|
apiClient := session.APIClient()
|
|
pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient)
|
|
|
|
// Delete star record from user's PDS
|
|
rkey := atproto.StarRecordKey(ownerDID, repository)
|
|
slog.Debug("Deleting star record", "handle", handle, "repository", repository, "rkey", rkey)
|
|
err = pdsClient.DeleteRecord(r.Context(), atproto.StarCollection, rkey)
|
|
if err != nil {
|
|
// If record doesn't exist, still return success (idempotent)
|
|
if !errors.Is(err, atproto.ErrRecordNotFound) {
|
|
slog.Error("Failed to delete star record", "error", err)
|
|
http.Error(w, fmt.Sprintf("Failed to delete star: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
slog.Debug("Star record not found, already unstarred")
|
|
}
|
|
|
|
// Return success
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]bool{"starred": false})
|
|
}
|
|
|
|
// CheckStarHandler checks if current user has starred a repository
|
|
type CheckStarHandler struct {
|
|
DB *sql.DB
|
|
Directory identity.Directory
|
|
Refresher *oauth.Refresher
|
|
}
|
|
|
|
func (h *CheckStarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Get authenticated user from middleware
|
|
user := middleware.GetUser(r)
|
|
if user == nil {
|
|
// Not authenticated - return not starred
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]bool{"starred": false})
|
|
return
|
|
}
|
|
|
|
// Extract parameters
|
|
handle := chi.URLParam(r, "handle")
|
|
repository := chi.URLParam(r, "repository")
|
|
|
|
// Resolve owner's handle to DID
|
|
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
|
|
if err != nil {
|
|
slog.Warn("Failed to resolve handle for check star", "handle", handle, "error", err)
|
|
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get OAuth session for the authenticated user
|
|
session, err := h.Refresher.GetSession(r.Context(), user.DID)
|
|
if err != nil {
|
|
slog.Debug("Failed to get OAuth session for check star", "user_did", user.DID, "error", err)
|
|
// No OAuth session - return not starred
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]bool{"starred": false})
|
|
return
|
|
}
|
|
|
|
// Get user's PDS client (use indigo's API client which handles DPoP automatically)
|
|
apiClient := session.APIClient()
|
|
pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient)
|
|
|
|
// Check if star record exists
|
|
rkey := atproto.StarRecordKey(ownerDID, repository)
|
|
_, err = pdsClient.GetRecord(r.Context(), atproto.StarCollection, rkey)
|
|
|
|
starred := err == nil
|
|
|
|
// Return result
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]bool{"starred": starred})
|
|
}
|
|
|
|
// GetStatsHandler returns repository statistics
|
|
type GetStatsHandler struct {
|
|
DB *sql.DB
|
|
Directory identity.Directory
|
|
}
|
|
|
|
func (h *GetStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Extract parameters
|
|
handle := chi.URLParam(r, "handle")
|
|
repository := chi.URLParam(r, "repository")
|
|
|
|
// Resolve owner's handle to DID
|
|
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
|
|
if err != nil {
|
|
http.Error(w, "Failed to resolve handle", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get repository stats from database
|
|
stats, err := db.GetRepositoryStats(h.DB, ownerDID, repository)
|
|
if err != nil {
|
|
http.Error(w, "Failed to fetch stats", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Return stats as JSON
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(stats)
|
|
}
|
|
|
|
// ManifestDetailHandler returns detailed manifest information including platforms
|
|
type ManifestDetailHandler struct {
|
|
DB *sql.DB
|
|
Directory identity.Directory
|
|
}
|
|
|
|
func (h *ManifestDetailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Extract parameters
|
|
handle := chi.URLParam(r, "handle")
|
|
repository := chi.URLParam(r, "repository")
|
|
digest := chi.URLParam(r, "digest")
|
|
|
|
// Resolve owner's handle to DID
|
|
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
|
|
if err != nil {
|
|
http.Error(w, "Failed to resolve handle", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get manifest detail from database
|
|
manifest, err := db.GetManifestDetail(h.DB, ownerDID, repository, digest)
|
|
if err != nil {
|
|
if err.Error() == "manifest not found" {
|
|
http.Error(w, "Manifest not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
slog.Error("Failed to get manifest detail", "error", err)
|
|
http.Error(w, "Failed to fetch manifest", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Return manifest as JSON
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(manifest)
|
|
}
|