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 } 58 59 s.Pages.Spindles(w, pages.SpindlesParams{ 60 LoggedInUser: user, 61 Spindles: all, 62 }) 63} 64 65// this endpoint inserts a record on behalf of the user to register that domain 66// 67// when registered, it also makes a request to see if the spindle declares this users as its owner, 68// and if so, marks the spindle as verified. 69// 70// if the spindle is not up yet, the user is free to retry verification at a later point 71func (s *Spindles) register(w http.ResponseWriter, r *http.Request) { 72 user := s.OAuth.GetUser(r) 73 l := s.Logger.With("handler", "register") 74 75 noticeId := "register-error" 76 defaultErr := "Failed to register spindle. Try again later." 77 fail := func() { 78 s.Pages.Notice(w, noticeId, defaultErr) 79 } 80 81 instance := r.FormValue("instance") 82 if instance == "" { 83 s.Pages.Notice(w, noticeId, "Incomplete form.") 84 return 85 } 86 87 tx, err := s.Db.Begin() 88 if err != nil { 89 l.Error("failed to start transaction", "err", err) 90 fail() 91 return 92 } 93 defer tx.Rollback() 94 95 err = db.AddSpindle(tx, db.Spindle{ 96 Owner: syntax.DID(user.Did), 97 Instance: instance, 98 }) 99 if err != nil { 100 l.Error("failed to insert", "err", err) 101 fail() 102 return 103 } 104 105 // create record on pds 106 client, err := s.OAuth.AuthorizedClient(r) 107 if err != nil { 108 l.Error("failed to authorize client", "err", err) 109 fail() 110 return 111 } 112 113 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 114 Collection: tangled.SpindleNSID, 115 Repo: user.Did, 116 Rkey: instance, 117 Record: &lexutil.LexiconTypeDecoder{ 118 Val: &tangled.Spindle{ 119 CreatedAt: time.Now().Format(time.RFC3339), 120 }, 121 }, 122 }) 123 if err != nil { 124 l.Error("failed to put record", "err", err) 125 fail() 126 return 127 } 128 129 err = tx.Commit() 130 if err != nil { 131 l.Error("failed to commit transaction", "err", err) 132 fail() 133 return 134 } 135 136 // begin verification 137 expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev) 138 if err != nil { 139 l.Error("verification failed", "err", err) 140 141 // just refresh the page 142 s.Pages.HxRefresh(w) 143 return 144 } 145 146 if expectedOwner != user.Did { 147 // verification failed 148 l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did) 149 s.Pages.HxRefresh(w) 150 return 151 } 152 153 // ok 154 s.Pages.HxRefresh(w) 155 return 156} 157 158func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { 159 user := s.OAuth.GetUser(r) 160 l := s.Logger.With("handler", "register") 161 162 noticeId := "operation-error" 163 defaultErr := "Failed to delete spindle. Try again later." 164 fail := func() { 165 s.Pages.Notice(w, noticeId, defaultErr) 166 } 167 168 instance := chi.URLParam(r, "instance") 169 if instance == "" { 170 l.Error("empty instance") 171 fail() 172 return 173 } 174 175 tx, err := s.Db.Begin() 176 if err != nil { 177 l.Error("failed to start txn", "err", err) 178 fail() 179 return 180 } 181 defer tx.Rollback() 182 183 err = db.DeleteSpindle( 184 tx, 185 db.FilterEq("owner", user.Did), 186 db.FilterEq("instance", instance), 187 ) 188 if err != nil { 189 l.Error("failed to delete spindle", "err", err) 190 fail() 191 return 192 } 193 194 client, err := s.OAuth.AuthorizedClient(r) 195 if err != nil { 196 l.Error("failed to authorize client", "err", err) 197 fail() 198 return 199 } 200 201 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 202 Collection: tangled.SpindleNSID, 203 Repo: user.Did, 204 Rkey: instance, 205 }) 206 if err != nil { 207 // non-fatal 208 l.Error("failed to delete record", "err", err) 209 } 210 211 err = tx.Commit() 212 if err != nil { 213 l.Error("failed to delete spindle", "err", err) 214 fail() 215 return 216 } 217 218 w.Write([]byte{}) 219} 220 221func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { 222 user := s.OAuth.GetUser(r) 223 l := s.Logger.With("handler", "register") 224 225 noticeId := "operation-error" 226 defaultErr := "Failed to verify spindle. Try again later." 227 fail := func() { 228 s.Pages.Notice(w, noticeId, defaultErr) 229 } 230 231 instance := chi.URLParam(r, "instance") 232 if instance == "" { 233 l.Error("empty instance") 234 fail() 235 return 236 } 237 238 // begin verification 239 expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev) 240 if err != nil { 241 l.Error("verification failed", "err", err) 242 fail() 243 return 244 } 245 246 if expectedOwner != user.Did { 247 l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did) 248 s.Pages.Notice(w, noticeId, fmt.Sprintf("Owner did not match, expected %s, got %s", expectedOwner, user.Did)) 249 return 250 } 251 252 // mark this spindle as verified in the db 253 rowId, err := db.VerifySpindle( 254 s.Db, 255 db.FilterEq("owner", user.Did), 256 db.FilterEq("instance", instance), 257 ) 258 259 verifiedSpindle := db.Spindle{ 260 Id: int(rowId), 261 Owner: syntax.DID(user.Did), 262 Instance: instance, 263 } 264 265 w.Header().Set("HX-Reswap", "outerHTML") 266 s.Pages.SpindleListing(w, pages.SpindleListingParams{ 267 LoggedInUser: user, 268 Spindle: verifiedSpindle, 269 }) 270} 271 272func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { 273 scheme := "https" 274 if dev { 275 scheme = "http" 276 } 277 278 url := fmt.Sprintf("%s://%s/owner", scheme, domain) 279 req, err := http.NewRequest("GET", url, nil) 280 if err != nil { 281 return "", err 282 } 283 284 client := &http.Client{ 285 Timeout: 1 * time.Second, 286 } 287 288 resp, err := client.Do(req.WithContext(ctx)) 289 if err != nil || resp.StatusCode != 200 { 290 return "", errors.New("failed to fetch /owner") 291 } 292 293 body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 294 if err != nil { 295 return "", fmt.Errorf("failed to read /owner response: %w", err) 296 } 297 298 did := strings.TrimSpace(string(body)) 299 if did == "" { 300 return "", errors.New("empty DID in /owner response") 301 } 302 303 return did, nil 304}