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}