+3
appview/config/config.go
+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
+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) {