Files
at-container-registry/pkg/appview/handlers/api.go

241 lines
7.6 KiB
Go

package handlers
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"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/bluesky-social/indigo/atproto/syntax"
"github.com/gorilla/mux"
)
// 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
vars := mux.Vars(r)
handle := vars["handle"]
repository := vars["repository"]
// Resolve owner's handle to DID
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
if err != nil {
log.Printf("StarRepository: Failed to resolve handle %s: %v", handle, err)
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
return
}
// Get OAuth session for the authenticated user
log.Printf("StarRepository: Getting OAuth session for user DID %s", user.DID)
session, err := h.Refresher.GetSession(r.Context(), user.DID)
if err != nil {
log.Printf("StarRepository: Failed to get OAuth session for %s: %v", user.DID, 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 {
log.Printf("StarRepository: Failed to create star record: %v", 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
vars := mux.Vars(r)
handle := vars["handle"]
repository := vars["repository"]
// Resolve owner's handle to DID
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
if err != nil {
log.Printf("UnstarRepository: Failed to resolve handle %s: %v", handle, err)
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
return
}
// Get OAuth session for the authenticated user
log.Printf("UnstarRepository: Getting OAuth session for user DID %s", user.DID)
session, err := h.Refresher.GetSession(r.Context(), user.DID)
if err != nil {
log.Printf("UnstarRepository: Failed to get OAuth session for %s: %v", user.DID, 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)
log.Printf("UnstarRepository: Deleting star record for %s/%s (rkey: %s)", handle, repository, rkey)
err = pdsClient.DeleteRecord(r.Context(), atproto.StarCollection, rkey)
if err != nil {
// If record doesn't exist, still return success (idempotent)
if err.Error() != "record not found" {
log.Printf("UnstarRepository: Failed to delete star record: %v", err)
http.Error(w, fmt.Sprintf("Failed to delete star: %v", err), http.StatusInternalServerError)
return
}
log.Printf("UnstarRepository: 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
vars := mux.Vars(r)
handle := vars["handle"]
repository := vars["repository"]
// Resolve owner's handle to DID
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
if err != nil {
log.Printf("CheckStar: Failed to resolve handle %s: %v", handle, 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 {
log.Printf("CheckStar: Failed to get OAuth session for %s: %v", user.DID, 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
vars := mux.Vars(r)
handle := vars["handle"]
repository := vars["repository"]
// Resolve owner's handle to DID
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, 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)
}
// resolveIdentityToDID is a helper function that resolves a handle or DID to a DID
func resolveIdentityToDID(ctx context.Context, directory identity.Directory, identityStr string) (string, error) {
// Parse as AT identifier (handle or DID)
atID, err := syntax.ParseAtIdentifier(identityStr)
if err != nil {
return "", err
}
// Resolve to DID via directory
ident, err := directory.Lookup(ctx, *atID)
if err != nil {
return "", err
}
return ident.DID.String(), nil
}