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