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}