forked from tangled.org/core
Monorepo for Tangled

appview/oauth: add to default spindle

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

authored by anirudh.fi and committed by Tangled 3fb08dd7 15f63d03

Changed files
+144
appview
config
oauth
handler
+3
appview/config/config.go
··· 16 16 AppviewHost string `env:"APPVIEW_HOST, default=https://tangled.sh"` 17 17 Dev bool `env:"DEV, default=false"` 18 18 DisallowedNicknamesFile string `env:"DISALLOWED_NICKNAMES_FILE"` 19 + 20 + // temporarily, to add users to default spindle 21 + AppPassword string `env:"APP_PASSWORD"` 19 22 } 20 23 21 24 type OAuthConfig struct {
+141
appview/oauth/handler/handler.go
··· 1 1 package oauth 2 2 3 3 import ( 4 + "bytes" 5 + "context" 4 6 "encoding/json" 5 7 "fmt" 6 8 "log" 7 9 "net/http" 8 10 "net/url" 9 11 "strings" 12 + "time" 10 13 11 14 "github.com/go-chi/chi/v5" 12 15 "github.com/gorilla/sessions" 13 16 "github.com/lestrrat-go/jwx/v2/jwk" 14 17 "github.com/posthog/posthog-go" 15 18 "tangled.sh/icyphox.sh/atproto-oauth/helpers" 19 + tangled "tangled.sh/tangled.sh/core/api/tangled" 16 20 sessioncache "tangled.sh/tangled.sh/core/appview/cache/session" 17 21 "tangled.sh/tangled.sh/core/appview/config" 18 22 "tangled.sh/tangled.sh/core/appview/db" ··· 23 27 "tangled.sh/tangled.sh/core/idresolver" 24 28 "tangled.sh/tangled.sh/core/knotclient" 25 29 "tangled.sh/tangled.sh/core/rbac" 30 + "tangled.sh/tangled.sh/core/tid" 26 31 ) 27 32 28 33 const ( ··· 294 299 295 300 log.Println("session saved successfully") 296 301 go o.addToDefaultKnot(oauthRequest.Did) 302 + go o.addToDefaultSpindle(oauthRequest.Did) 297 303 298 304 if !o.config.Core.Dev { 299 305 err = o.posthog.Enqueue(posthog.Capture{ ··· 330 336 return nil, err 331 337 } 332 338 return pubKey, nil 339 + } 340 + 341 + func (o *OAuthHandler) addToDefaultSpindle(did string) { 342 + // use the tangled.sh app password to get an accessJwt 343 + // and create an sh.tangled.spindle.member record with that 344 + 345 + defaultSpindle := "spindle.tangled.sh" 346 + appPassword := o.config.Core.AppPassword 347 + 348 + spindleMembers, err := db.GetSpindleMembers( 349 + o.db, 350 + db.FilterEq("instance", "spindle.tangled.sh"), 351 + db.FilterEq("subject", did), 352 + ) 353 + if err != nil { 354 + log.Printf("failed to get spindle members for did %s: %v", did, err) 355 + return 356 + } 357 + 358 + if len(spindleMembers) != 0 { 359 + log.Printf("did %s is already a member of the default spindle", did) 360 + return 361 + } 362 + 363 + // TODO: hardcoded tangled handle and did for now 364 + tangledHandle := "tangled.sh" 365 + tangledDid := "did:plc:wshs7t2adsemcrrd4snkeqli" 366 + 367 + if appPassword == "" { 368 + log.Println("no app password configured, skipping spindle member addition") 369 + return 370 + } 371 + 372 + log.Printf("adding %s to default spindle", did) 373 + 374 + resolved, err := o.idResolver.ResolveIdent(context.Background(), tangledDid) 375 + if err != nil { 376 + log.Printf("failed to resolve tangled.sh DID %s: %v", tangledDid, err) 377 + return 378 + } 379 + 380 + pdsEndpoint := resolved.PDSEndpoint() 381 + if pdsEndpoint == "" { 382 + log.Printf("no PDS endpoint found for tangled.sh DID %s", tangledDid) 383 + return 384 + } 385 + 386 + sessionPayload := map[string]string{ 387 + "identifier": tangledHandle, 388 + "password": appPassword, 389 + } 390 + sessionBytes, err := json.Marshal(sessionPayload) 391 + if err != nil { 392 + log.Printf("failed to marshal session payload: %v", err) 393 + return 394 + } 395 + 396 + sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession" 397 + sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes)) 398 + if err != nil { 399 + log.Printf("failed to create session request: %v", err) 400 + return 401 + } 402 + sessionReq.Header.Set("Content-Type", "application/json") 403 + 404 + client := &http.Client{Timeout: 30 * time.Second} 405 + sessionResp, err := client.Do(sessionReq) 406 + if err != nil { 407 + log.Printf("failed to create session: %v", err) 408 + return 409 + } 410 + defer sessionResp.Body.Close() 411 + 412 + if sessionResp.StatusCode != http.StatusOK { 413 + log.Printf("failed to create session: HTTP %d", sessionResp.StatusCode) 414 + return 415 + } 416 + 417 + var session struct { 418 + AccessJwt string `json:"accessJwt"` 419 + } 420 + if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil { 421 + log.Printf("failed to decode session response: %v", err) 422 + return 423 + } 424 + 425 + record := tangled.SpindleMember{ 426 + LexiconTypeID: "sh.tangled.spindle.member", 427 + Subject: did, 428 + Instance: defaultSpindle, 429 + CreatedAt: time.Now().Format(time.RFC3339), 430 + } 431 + 432 + recordBytes, err := json.Marshal(record) 433 + if err != nil { 434 + log.Printf("failed to marshal spindle member record: %v", err) 435 + return 436 + } 437 + 438 + payload := map[string]interface{}{ 439 + "repo": tangledDid, 440 + "collection": tangled.SpindleMemberNSID, 441 + "rkey": tid.TID(), 442 + "record": json.RawMessage(recordBytes), 443 + } 444 + 445 + payloadBytes, err := json.Marshal(payload) 446 + if err != nil { 447 + log.Printf("failed to marshal request payload: %v", err) 448 + return 449 + } 450 + 451 + url := pdsEndpoint + "/xrpc/com.atproto.repo.putRecord" 452 + req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes)) 453 + if err != nil { 454 + log.Printf("failed to create HTTP request: %v", err) 455 + return 456 + } 457 + 458 + req.Header.Set("Content-Type", "application/json") 459 + req.Header.Set("Authorization", "Bearer "+session.AccessJwt) 460 + 461 + resp, err := client.Do(req) 462 + if err != nil { 463 + log.Printf("failed to add user to default spindle: %v", err) 464 + return 465 + } 466 + defer resp.Body.Close() 467 + 468 + if resp.StatusCode != http.StatusOK { 469 + log.Printf("failed to add user to default spindle: HTTP %d", resp.StatusCode) 470 + return 471 + } 472 + 473 + log.Printf("successfully added %s to default spindle", did) 333 474 } 334 475 335 476 func (o *OAuthHandler) addToDefaultKnot(did string) {