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}