this repo has no description
1package spindles 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "log/slog" 9 "net/http" 10 "strings" 11 "time" 12 13 "github.com/go-chi/chi/v5" 14 "tangled.sh/tangled.sh/core/api/tangled" 15 "tangled.sh/tangled.sh/core/appview/config" 16 "tangled.sh/tangled.sh/core/appview/db" 17 "tangled.sh/tangled.sh/core/appview/middleware" 18 "tangled.sh/tangled.sh/core/appview/oauth" 19 "tangled.sh/tangled.sh/core/appview/pages" 20 "tangled.sh/tangled.sh/core/rbac" 21 22 comatproto "github.com/bluesky-social/indigo/api/atproto" 23 "github.com/bluesky-social/indigo/atproto/syntax" 24 lexutil "github.com/bluesky-social/indigo/lex/util" 25) 26 27type Spindles struct { 28 Db *db.DB 29 OAuth *oauth.OAuth 30 Pages *pages.Pages 31 Config *config.Config 32 Enforcer *rbac.Enforcer 33 Logger *slog.Logger 34} 35 36func (s *Spindles) Router() http.Handler { 37 r := chi.NewRouter() 38 39 r.Use(middleware.AuthMiddleware(s.OAuth)) 40 41 r.Get("/", s.spindles) 42 r.Post("/register", s.register) 43 r.Delete("/{instance}", s.delete) 44 r.Post("/{instance}/retry", s.retry) 45 46 return r 47} 48 49func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) { 50 user := s.OAuth.GetUser(r) 51 all, err := db.GetSpindles( 52 s.Db, 53 db.FilterEq("owner", user.Did), 54 ) 55 if err != nil { 56 s.Logger.Error("failed to fetch spindles", "err", err) 57 w.WriteHeader(http.StatusInternalServerError) 58 return 59 } 60 61 s.Pages.Spindles(w, pages.SpindlesParams{ 62 LoggedInUser: user, 63 Spindles: all, 64 }) 65} 66 67// this endpoint inserts a record on behalf of the user to register that domain 68// 69// when registered, it also makes a request to see if the spindle declares this users as its owner, 70// and if so, marks the spindle as verified. 71// 72// if the spindle is not up yet, the user is free to retry verification at a later point 73func (s *Spindles) register(w http.ResponseWriter, r *http.Request) { 74 user := s.OAuth.GetUser(r) 75 l := s.Logger.With("handler", "register") 76 77 noticeId := "register-error" 78 defaultErr := "Failed to register spindle. Try again later." 79 fail := func() { 80 s.Pages.Notice(w, noticeId, defaultErr) 81 } 82 83 instance := r.FormValue("instance") 84 if instance == "" { 85 s.Pages.Notice(w, noticeId, "Incomplete form.") 86 return 87 } 88 89 tx, err := s.Db.Begin() 90 if err != nil { 91 l.Error("failed to start transaction", "err", err) 92 fail() 93 return 94 } 95 defer tx.Rollback() 96 97 err = db.AddSpindle(tx, db.Spindle{ 98 Owner: syntax.DID(user.Did), 99 Instance: instance, 100 }) 101 if err != nil { 102 l.Error("failed to insert", "err", err) 103 fail() 104 return 105 } 106 107 // create record on pds 108 client, err := s.OAuth.AuthorizedClient(r) 109 if err != nil { 110 l.Error("failed to authorize client", "err", err) 111 fail() 112 return 113 } 114 115 ex, _ := client.RepoGetRecord(r.Context(), "", tangled.SpindleNSID, user.Did, instance) 116 var exCid *string 117 if ex != nil { 118 exCid = ex.Cid 119 } 120 121 // re-announce by registering under same rkey 122 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 123 Collection: tangled.SpindleNSID, 124 Repo: user.Did, 125 Rkey: instance, 126 Record: &lexutil.LexiconTypeDecoder{ 127 Val: &tangled.Spindle{ 128 CreatedAt: time.Now().Format(time.RFC3339), 129 }, 130 }, 131 SwapRecord: exCid, 132 }) 133 134 if err != nil { 135 l.Error("failed to put record", "err", err) 136 fail() 137 return 138 } 139 140 err = tx.Commit() 141 if err != nil { 142 l.Error("failed to commit transaction", "err", err) 143 fail() 144 return 145 } 146 147 // begin verification 148 expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev) 149 if err != nil { 150 l.Error("verification failed", "err", err) 151 152 // just refresh the page 153 s.Pages.HxRefresh(w) 154 return 155 } 156 157 if expectedOwner != user.Did { 158 // verification failed 159 l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did) 160 s.Pages.HxRefresh(w) 161 return 162 } 163 164 tx, err = s.Db.Begin() 165 if err != nil { 166 l.Error("failed to commit verification info", "err", err) 167 s.Pages.HxRefresh(w) 168 return 169 } 170 defer func() { 171 tx.Rollback() 172 s.Enforcer.E.LoadPolicy() 173 }() 174 175 // mark this spindle as verified in the db 176 _, err = db.VerifySpindle( 177 tx, 178 db.FilterEq("owner", user.Did), 179 db.FilterEq("instance", instance), 180 ) 181 182 err = s.Enforcer.AddSpindleOwner(instance, user.Did) 183 if err != nil { 184 l.Error("failed to update ACL", "err", err) 185 s.Pages.HxRefresh(w) 186 return 187 } 188 189 err = tx.Commit() 190 if err != nil { 191 l.Error("failed to commit verification info", "err", err) 192 s.Pages.HxRefresh(w) 193 return 194 } 195 196 err = s.Enforcer.E.SavePolicy() 197 if err != nil { 198 l.Error("failed to update ACL", "err", err) 199 s.Pages.HxRefresh(w) 200 return 201 } 202 203 // ok 204 s.Pages.HxRefresh(w) 205 return 206} 207 208func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { 209 user := s.OAuth.GetUser(r) 210 l := s.Logger.With("handler", "register") 211 212 noticeId := "operation-error" 213 defaultErr := "Failed to delete spindle. Try again later." 214 fail := func() { 215 s.Pages.Notice(w, noticeId, defaultErr) 216 } 217 218 instance := chi.URLParam(r, "instance") 219 if instance == "" { 220 l.Error("empty instance") 221 fail() 222 return 223 } 224 225 tx, err := s.Db.Begin() 226 if err != nil { 227 l.Error("failed to start txn", "err", err) 228 fail() 229 return 230 } 231 defer tx.Rollback() 232 233 err = db.DeleteSpindle( 234 tx, 235 db.FilterEq("owner", user.Did), 236 db.FilterEq("instance", instance), 237 ) 238 if err != nil { 239 l.Error("failed to delete spindle", "err", err) 240 fail() 241 return 242 } 243 244 client, err := s.OAuth.AuthorizedClient(r) 245 if err != nil { 246 l.Error("failed to authorize client", "err", err) 247 fail() 248 return 249 } 250 251 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 252 Collection: tangled.SpindleNSID, 253 Repo: user.Did, 254 Rkey: instance, 255 }) 256 if err != nil { 257 // non-fatal 258 l.Error("failed to delete record", "err", err) 259 } 260 261 err = tx.Commit() 262 if err != nil { 263 l.Error("failed to delete spindle", "err", err) 264 fail() 265 return 266 } 267 268 w.Write([]byte{}) 269} 270 271func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { 272 user := s.OAuth.GetUser(r) 273 l := s.Logger.With("handler", "register") 274 275 noticeId := "operation-error" 276 defaultErr := "Failed to verify spindle. Try again later." 277 fail := func() { 278 s.Pages.Notice(w, noticeId, defaultErr) 279 } 280 281 instance := chi.URLParam(r, "instance") 282 if instance == "" { 283 l.Error("empty instance") 284 fail() 285 return 286 } 287 288 // begin verification 289 expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev) 290 if err != nil { 291 l.Error("verification failed", "err", err) 292 fail() 293 return 294 } 295 296 if expectedOwner != user.Did { 297 l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did) 298 s.Pages.Notice(w, noticeId, fmt.Sprintf("Owner did not match, expected %s, got %s", expectedOwner, user.Did)) 299 return 300 } 301 302 // mark this spindle as verified in the db 303 rowId, err := db.VerifySpindle( 304 s.Db, 305 db.FilterEq("owner", user.Did), 306 db.FilterEq("instance", instance), 307 ) 308 if err != nil { 309 l.Error("verification failed", "err", err) 310 fail() 311 return 312 } 313 314 verifiedSpindle := db.Spindle{ 315 Id: int(rowId), 316 Owner: syntax.DID(user.Did), 317 Instance: instance, 318 } 319 320 w.Header().Set("HX-Reswap", "outerHTML") 321 s.Pages.SpindleListing(w, pages.SpindleListingParams{ 322 LoggedInUser: user, 323 Spindle: verifiedSpindle, 324 }) 325} 326 327func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { 328 scheme := "https" 329 if dev { 330 scheme = "http" 331 } 332 333 url := fmt.Sprintf("%s://%s/owner", scheme, domain) 334 req, err := http.NewRequest("GET", url, nil) 335 if err != nil { 336 return "", err 337 } 338 339 client := &http.Client{ 340 Timeout: 1 * time.Second, 341 } 342 343 resp, err := client.Do(req.WithContext(ctx)) 344 if err != nil || resp.StatusCode != 200 { 345 return "", errors.New("failed to fetch /owner") 346 } 347 348 body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 349 if err != nil { 350 return "", fmt.Errorf("failed to read /owner response: %w", err) 351 } 352 353 did := strings.TrimSpace(string(body)) 354 if did == "" { 355 return "", errors.New("empty DID in /owner response") 356 } 357 358 return did, nil 359}