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