A community based topic aggregation platform built on atproto
1package routes 2 3import ( 4 "encoding/json" 5 "errors" 6 "log" 7 "net/http" 8 "time" 9 10 "Coves/internal/core/users" 11 12 "github.com/go-chi/chi/v5" 13) 14 15// UserHandler handles user-related XRPC endpoints 16type UserHandler struct { 17 userService users.UserService 18} 19 20// NewUserHandler creates a new user handler 21func NewUserHandler(userService users.UserService) *UserHandler { 22 return &UserHandler{ 23 userService: userService, 24 } 25} 26 27// RegisterUserRoutes registers user-related XRPC endpoints on the router 28// Implements social.coves.actor.* lexicon endpoints 29func RegisterUserRoutes(r chi.Router, service users.UserService) { 30 h := NewUserHandler(service) 31 32 // social.coves.actor.getProfile - query endpoint 33 r.Get("/xrpc/social.coves.actor.getProfile", h.GetProfile) 34 35 // social.coves.actor.signup - procedure endpoint 36 r.Post("/xrpc/social.coves.actor.signup", h.Signup) 37} 38 39// GetProfile handles social.coves.actor.getProfile 40// Query endpoint that retrieves a user profile by DID or handle 41func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) { 42 ctx := r.Context() 43 44 // Get actor parameter (DID or handle) 45 actor := r.URL.Query().Get("actor") 46 if actor == "" { 47 http.Error(w, "actor parameter is required", http.StatusBadRequest) 48 return 49 } 50 51 var user *users.User 52 var err error 53 54 // Determine if actor is a DID or handle 55 // DIDs start with "did:", handles don't 56 if len(actor) > 4 && actor[:4] == "did:" { 57 user, err = h.userService.GetUserByDID(ctx, actor) 58 } else { 59 user, err = h.userService.GetUserByHandle(ctx, actor) 60 } 61 62 if err != nil { 63 http.Error(w, "user not found", http.StatusNotFound) 64 return 65 } 66 67 // Minimal profile response (matching lexicon structure) 68 response := map[string]interface{}{ 69 "did": user.DID, 70 "profile": map[string]interface{}{ 71 "handle": user.Handle, 72 "createdAt": user.CreatedAt.Format(time.RFC3339), 73 }, 74 } 75 76 w.Header().Set("Content-Type", "application/json") 77 w.WriteHeader(http.StatusOK) 78 if err := json.NewEncoder(w).Encode(response); err != nil { 79 log.Printf("Failed to encode response: %v", err) 80 } 81} 82 83// Signup handles social.coves.actor.signup 84// Procedure endpoint that registers a new account on the Coves instance 85func (h *UserHandler) Signup(w http.ResponseWriter, r *http.Request) { 86 ctx := r.Context() 87 88 // Parse request body 89 var req users.RegisterAccountRequest 90 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 91 http.Error(w, "invalid request body", http.StatusBadRequest) 92 return 93 } 94 95 // Call service to register account 96 resp, err := h.userService.RegisterAccount(ctx, req) 97 if err != nil { 98 // Map service errors to lexicon error types with proper HTTP status codes 99 respondWithLexiconError(w, err) 100 return 101 } 102 103 // Return response matching lexicon output schema 104 response := map[string]interface{}{ 105 "did": resp.DID, 106 "handle": resp.Handle, 107 "accessJwt": resp.AccessJwt, 108 "refreshJwt": resp.RefreshJwt, 109 } 110 111 w.Header().Set("Content-Type", "application/json") 112 w.WriteHeader(http.StatusOK) 113 if err := json.NewEncoder(w).Encode(response); err != nil { 114 log.Printf("Failed to encode response: %v", err) 115 } 116} 117 118// respondWithLexiconError maps domain errors to lexicon error types and HTTP status codes 119// Error names match the lexicon definition in social.coves.actor.signup 120func respondWithLexiconError(w http.ResponseWriter, err error) { 121 var ( 122 statusCode int 123 errorName string 124 message string 125 ) 126 127 // Map domain errors to lexicon error types 128 var invalidHandleErr *users.InvalidHandleError 129 var handleNotAvailableErr *users.HandleNotAvailableError 130 var invalidInviteCodeErr *users.InvalidInviteCodeError 131 var invalidEmailErr *users.InvalidEmailError 132 var weakPasswordErr *users.WeakPasswordError 133 var pdsErr *users.PDSError 134 135 switch { 136 case errors.As(err, &invalidHandleErr): 137 statusCode = http.StatusBadRequest 138 errorName = "InvalidHandle" 139 message = invalidHandleErr.Error() 140 141 case errors.As(err, &handleNotAvailableErr): 142 statusCode = http.StatusBadRequest 143 errorName = "HandleNotAvailable" 144 message = handleNotAvailableErr.Error() 145 146 case errors.As(err, &invalidInviteCodeErr): 147 statusCode = http.StatusBadRequest 148 errorName = "InvalidInviteCode" 149 message = invalidInviteCodeErr.Error() 150 151 case errors.As(err, &invalidEmailErr): 152 statusCode = http.StatusBadRequest 153 errorName = "InvalidEmail" 154 message = invalidEmailErr.Error() 155 156 case errors.As(err, &weakPasswordErr): 157 statusCode = http.StatusBadRequest 158 errorName = "WeakPassword" 159 message = weakPasswordErr.Error() 160 161 case errors.As(err, &pdsErr): 162 // PDS errors get mapped based on status code 163 statusCode = pdsErr.StatusCode 164 errorName = "PDSError" 165 message = pdsErr.Message 166 167 default: 168 // Generic error handling (avoid leaking internal details) 169 statusCode = http.StatusInternalServerError 170 errorName = "InternalServerError" 171 message = "An error occurred while processing your request" 172 } 173 174 // XRPC error response format 175 w.Header().Set("Content-Type", "application/json") 176 w.WriteHeader(statusCode) 177 if err := json.NewEncoder(w).Encode(map[string]interface{}{ 178 "error": errorName, 179 "message": message, 180 }); err != nil { 181 log.Printf("Failed to encode error response: %v", err) 182 } 183}