+79
-20
api/tangled/cbor_gen.go
+79
-20
api/tangled/cbor_gen.go
···
7934
7934
}
7935
7935
7936
7936
cw := cbg.NewCborWriter(w)
7937
-
fieldCount := 9
7937
+
fieldCount := 10
7938
7938
7939
7939
if t.Body == nil {
7940
7940
fieldCount--
7941
7941
}
7942
7942
7943
7943
if t.Mentions == nil {
7944
+
fieldCount--
7945
+
}
7946
+
7947
+
if t.Patch == nil {
7944
7948
fieldCount--
7945
7949
}
7946
7950
···
8008
8012
}
8009
8013
8010
8014
// t.Patch (string) (string)
8011
-
if len("patch") > 1000000 {
8012
-
return xerrors.Errorf("Value in field \"patch\" was too long")
8013
-
}
8015
+
if t.Patch != nil {
8014
8016
8015
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil {
8016
-
return err
8017
-
}
8018
-
if _, err := cw.WriteString(string("patch")); err != nil {
8019
-
return err
8020
-
}
8017
+
if len("patch") > 1000000 {
8018
+
return xerrors.Errorf("Value in field \"patch\" was too long")
8019
+
}
8021
8020
8022
-
if len(t.Patch) > 1000000 {
8023
-
return xerrors.Errorf("Value in field t.Patch was too long")
8024
-
}
8021
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil {
8022
+
return err
8023
+
}
8024
+
if _, err := cw.WriteString(string("patch")); err != nil {
8025
+
return err
8026
+
}
8027
+
8028
+
if t.Patch == nil {
8029
+
if _, err := cw.Write(cbg.CborNull); err != nil {
8030
+
return err
8031
+
}
8032
+
} else {
8033
+
if len(*t.Patch) > 1000000 {
8034
+
return xerrors.Errorf("Value in field t.Patch was too long")
8035
+
}
8025
8036
8026
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Patch))); err != nil {
8027
-
return err
8028
-
}
8029
-
if _, err := cw.WriteString(string(t.Patch)); err != nil {
8030
-
return err
8037
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Patch))); err != nil {
8038
+
return err
8039
+
}
8040
+
if _, err := cw.WriteString(string(*t.Patch)); err != nil {
8041
+
return err
8042
+
}
8043
+
}
8031
8044
}
8032
8045
8033
8046
// t.Title (string) (string)
···
8147
8160
return err
8148
8161
}
8149
8162
8163
+
// t.PatchBlob (util.LexBlob) (struct)
8164
+
if len("patchBlob") > 1000000 {
8165
+
return xerrors.Errorf("Value in field \"patchBlob\" was too long")
8166
+
}
8167
+
8168
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil {
8169
+
return err
8170
+
}
8171
+
if _, err := cw.WriteString(string("patchBlob")); err != nil {
8172
+
return err
8173
+
}
8174
+
8175
+
if err := t.PatchBlob.MarshalCBOR(cw); err != nil {
8176
+
return err
8177
+
}
8178
+
8150
8179
// t.References ([]string) (slice)
8151
8180
if t.References != nil {
8152
8181
···
8262
8291
case "patch":
8263
8292
8264
8293
{
8265
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8294
+
b, err := cr.ReadByte()
8266
8295
if err != nil {
8267
8296
return err
8268
8297
}
8298
+
if b != cbg.CborNull[0] {
8299
+
if err := cr.UnreadByte(); err != nil {
8300
+
return err
8301
+
}
8269
8302
8270
-
t.Patch = string(sval)
8303
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8304
+
if err != nil {
8305
+
return err
8306
+
}
8307
+
8308
+
t.Patch = (*string)(&sval)
8309
+
}
8271
8310
}
8272
8311
// t.Title (string) (string)
8273
8312
case "title":
···
8370
8409
}
8371
8410
8372
8411
t.CreatedAt = string(sval)
8412
+
}
8413
+
// t.PatchBlob (util.LexBlob) (struct)
8414
+
case "patchBlob":
8415
+
8416
+
{
8417
+
8418
+
b, err := cr.ReadByte()
8419
+
if err != nil {
8420
+
return err
8421
+
}
8422
+
if b != cbg.CborNull[0] {
8423
+
if err := cr.UnreadByte(); err != nil {
8424
+
return err
8425
+
}
8426
+
t.PatchBlob = new(util.LexBlob)
8427
+
if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil {
8428
+
return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err)
8429
+
}
8430
+
}
8431
+
8373
8432
}
8374
8433
// t.References ([]string) (slice)
8375
8434
case "references":
+12
-9
api/tangled/repopull.go
+12
-9
api/tangled/repopull.go
···
17
17
} //
18
18
// RECORDTYPE: RepoPull
19
19
type RepoPull struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
-
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
-
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
-
Patch string `json:"patch" cborgen:"patch"`
25
-
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
26
-
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
27
-
Target *RepoPull_Target `json:"target" cborgen:"target"`
28
-
Title string `json:"title" cborgen:"title"`
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
+
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
+
// patch: (deprecated) use patchBlob instead
25
+
Patch *string `json:"patch,omitempty" cborgen:"patch,omitempty"`
26
+
// patchBlob: patch content
27
+
PatchBlob *util.LexBlob `json:"patchBlob" cborgen:"patchBlob"`
28
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
29
+
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
30
+
Target *RepoPull_Target `json:"target" cborgen:"target"`
31
+
Title string `json:"title" cborgen:"title"`
29
32
}
30
33
31
34
// RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
+18
-11
appview/db/profile.go
+18
-11
appview/db/profile.go
···
20
20
timeline := models.ProfileTimeline{
21
21
ByMonth: make([]models.ByMonth, TimeframeMonths),
22
22
}
23
-
currentMonth := time.Now().Month()
23
+
now := time.Now()
24
24
timeframe := fmt.Sprintf("-%d months", TimeframeMonths)
25
25
26
26
pulls, err := GetPullsByOwnerDid(e, forDid, timeframe)
···
30
30
31
31
// group pulls by month
32
32
for _, pull := range pulls {
33
-
pullMonth := pull.Created.Month()
33
+
monthsAgo := monthsBetween(pull.Created, now)
34
34
35
-
if currentMonth-pullMonth >= TimeframeMonths {
35
+
if monthsAgo >= TimeframeMonths {
36
36
// shouldn't happen; but times are weird
37
37
continue
38
38
}
39
39
40
-
idx := currentMonth - pullMonth
40
+
idx := monthsAgo
41
41
items := &timeline.ByMonth[idx].PullEvents.Items
42
42
43
43
*items = append(*items, &pull)
···
53
53
}
54
54
55
55
for _, issue := range issues {
56
-
issueMonth := issue.Created.Month()
56
+
monthsAgo := monthsBetween(issue.Created, now)
57
57
58
-
if currentMonth-issueMonth >= TimeframeMonths {
58
+
if monthsAgo >= TimeframeMonths {
59
59
// shouldn't happen; but times are weird
60
60
continue
61
61
}
62
62
63
-
idx := currentMonth - issueMonth
63
+
idx := monthsAgo
64
64
items := &timeline.ByMonth[idx].IssueEvents.Items
65
65
66
66
*items = append(*items, &issue)
···
77
77
if repo.Source != "" {
78
78
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
79
79
if err != nil {
80
-
return nil, err
80
+
// the source repo was not found, skip this bit
81
+
log.Println("profile", "err", err)
81
82
}
82
83
}
83
84
84
-
repoMonth := repo.Created.Month()
85
+
monthsAgo := monthsBetween(repo.Created, now)
85
86
86
-
if currentMonth-repoMonth >= TimeframeMonths {
87
+
if monthsAgo >= TimeframeMonths {
87
88
// shouldn't happen; but times are weird
88
89
continue
89
90
}
90
91
91
-
idx := currentMonth - repoMonth
92
+
idx := monthsAgo
92
93
93
94
items := &timeline.ByMonth[idx].RepoEvents
94
95
*items = append(*items, models.RepoEvent{
···
98
99
}
99
100
100
101
return &timeline, nil
102
+
}
103
+
104
+
func monthsBetween(from, to time.Time) int {
105
+
years := to.Year() - from.Year()
106
+
months := int(to.Month() - from.Month())
107
+
return years*12 + months
101
108
}
102
109
103
110
func UpsertProfile(tx *sql.Tx, profile *models.Profile) error {
+1
-1
appview/db/punchcard.go
+1
-1
appview/db/punchcard.go
+32
-32
appview/issues/issues.go
+32
-32
appview/issues/issues.go
···
81
81
82
82
func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
83
83
l := rp.logger.With("handler", "RepoSingleIssue")
84
-
user := rp.oauth.GetUser(r)
84
+
user := rp.oauth.GetMultiAccountUser(r)
85
85
f, err := rp.repoResolver.Resolve(r)
86
86
if err != nil {
87
87
l.Error("failed to get repo and knot", "err", err)
···
102
102
103
103
userReactions := map[models.ReactionKind]bool{}
104
104
if user != nil {
105
-
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
105
+
userReactions = db.GetReactionStatusMap(rp.db, user.Active.Did, issue.AtUri())
106
106
}
107
107
108
108
backlinks, err := db.GetBacklinks(rp.db, issue.AtUri())
···
143
143
144
144
func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) {
145
145
l := rp.logger.With("handler", "EditIssue")
146
-
user := rp.oauth.GetUser(r)
146
+
user := rp.oauth.GetMultiAccountUser(r)
147
147
148
148
issue, ok := r.Context().Value("issue").(*models.Issue)
149
149
if !ok {
···
182
182
return
183
183
}
184
184
185
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Did, newIssue.Rkey)
185
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Active.Did, newIssue.Rkey)
186
186
if err != nil {
187
187
l.Error("failed to get record", "err", err)
188
188
rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.")
···
191
191
192
192
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
193
193
Collection: tangled.RepoIssueNSID,
194
-
Repo: user.Did,
194
+
Repo: user.Active.Did,
195
195
Rkey: newIssue.Rkey,
196
196
SwapRecord: ex.Cid,
197
197
Record: &lexutil.LexiconTypeDecoder{
···
292
292
293
293
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
294
294
l := rp.logger.With("handler", "CloseIssue")
295
-
user := rp.oauth.GetUser(r)
295
+
user := rp.oauth.GetMultiAccountUser(r)
296
296
f, err := rp.repoResolver.Resolve(r)
297
297
if err != nil {
298
298
l.Error("failed to get repo and knot", "err", err)
···
306
306
return
307
307
}
308
308
309
-
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
309
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
310
310
isRepoOwner := roles.IsOwner()
311
311
isCollaborator := roles.IsCollaborator()
312
-
isIssueOwner := user.Did == issue.Did
312
+
isIssueOwner := user.Active.Did == issue.Did
313
313
314
314
// TODO: make this more granular
315
315
if isIssueOwner || isRepoOwner || isCollaborator {
···
326
326
issue.Open = false
327
327
328
328
// notify about the issue closure
329
-
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
329
+
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue)
330
330
331
331
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
332
332
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
···
340
340
341
341
func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) {
342
342
l := rp.logger.With("handler", "ReopenIssue")
343
-
user := rp.oauth.GetUser(r)
343
+
user := rp.oauth.GetMultiAccountUser(r)
344
344
f, err := rp.repoResolver.Resolve(r)
345
345
if err != nil {
346
346
l.Error("failed to get repo and knot", "err", err)
···
354
354
return
355
355
}
356
356
357
-
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
357
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
358
358
isRepoOwner := roles.IsOwner()
359
359
isCollaborator := roles.IsCollaborator()
360
-
isIssueOwner := user.Did == issue.Did
360
+
isIssueOwner := user.Active.Did == issue.Did
361
361
362
362
if isCollaborator || isRepoOwner || isIssueOwner {
363
363
err := db.ReopenIssues(
···
373
373
issue.Open = true
374
374
375
375
// notify about the issue reopen
376
-
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
376
+
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue)
377
377
378
378
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
379
379
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
···
387
387
388
388
func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) {
389
389
l := rp.logger.With("handler", "NewIssueComment")
390
-
user := rp.oauth.GetUser(r)
390
+
user := rp.oauth.GetMultiAccountUser(r)
391
391
f, err := rp.repoResolver.Resolve(r)
392
392
if err != nil {
393
393
l.Error("failed to get repo and knot", "err", err)
···
416
416
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417
417
418
418
comment := models.IssueComment{
419
-
Did: user.Did,
419
+
Did: user.Active.Did,
420
420
Rkey: tid.TID(),
421
421
IssueAt: issue.AtUri().String(),
422
422
ReplyTo: replyTo,
···
495
495
496
496
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
497
497
l := rp.logger.With("handler", "IssueComment")
498
-
user := rp.oauth.GetUser(r)
498
+
user := rp.oauth.GetMultiAccountUser(r)
499
499
500
500
issue, ok := r.Context().Value("issue").(*models.Issue)
501
501
if !ok {
···
531
531
532
532
func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
533
533
l := rp.logger.With("handler", "EditIssueComment")
534
-
user := rp.oauth.GetUser(r)
534
+
user := rp.oauth.GetMultiAccountUser(r)
535
535
536
536
issue, ok := r.Context().Value("issue").(*models.Issue)
537
537
if !ok {
···
557
557
}
558
558
comment := comments[0]
559
559
560
-
if comment.Did != user.Did {
561
-
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
560
+
if comment.Did != user.Active.Did {
561
+
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did)
562
562
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
563
563
return
564
564
}
···
608
608
// rkey is optional, it was introduced later
609
609
if newComment.Rkey != "" {
610
610
// update the record on pds
611
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey)
611
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey)
612
612
if err != nil {
613
613
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
614
614
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
617
617
618
618
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
619
619
Collection: tangled.RepoIssueCommentNSID,
620
-
Repo: user.Did,
620
+
Repo: user.Active.Did,
621
621
Rkey: newComment.Rkey,
622
622
SwapRecord: ex.Cid,
623
623
Record: &lexutil.LexiconTypeDecoder{
···
641
641
642
642
func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) {
643
643
l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder")
644
-
user := rp.oauth.GetUser(r)
644
+
user := rp.oauth.GetMultiAccountUser(r)
645
645
646
646
issue, ok := r.Context().Value("issue").(*models.Issue)
647
647
if !ok {
···
677
677
678
678
func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) {
679
679
l := rp.logger.With("handler", "ReplyIssueComment")
680
-
user := rp.oauth.GetUser(r)
680
+
user := rp.oauth.GetMultiAccountUser(r)
681
681
682
682
issue, ok := r.Context().Value("issue").(*models.Issue)
683
683
if !ok {
···
713
713
714
714
func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
715
715
l := rp.logger.With("handler", "DeleteIssueComment")
716
-
user := rp.oauth.GetUser(r)
716
+
user := rp.oauth.GetMultiAccountUser(r)
717
717
718
718
issue, ok := r.Context().Value("issue").(*models.Issue)
719
719
if !ok {
···
739
739
}
740
740
comment := comments[0]
741
741
742
-
if comment.Did != user.Did {
743
-
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
742
+
if comment.Did != user.Active.Did {
743
+
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did)
744
744
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
745
745
return
746
746
}
···
769
769
}
770
770
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
771
771
Collection: tangled.RepoIssueCommentNSID,
772
-
Repo: user.Did,
772
+
Repo: user.Active.Did,
773
773
Rkey: comment.Rkey,
774
774
})
775
775
if err != nil {
···
807
807
808
808
page := pagination.FromContext(r.Context())
809
809
810
-
user := rp.oauth.GetUser(r)
810
+
user := rp.oauth.GetMultiAccountUser(r)
811
811
f, err := rp.repoResolver.Resolve(r)
812
812
if err != nil {
813
813
l.Error("failed to get repo and knot", "err", err)
···
884
884
}
885
885
886
886
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
887
-
LoggedInUser: rp.oauth.GetUser(r),
887
+
LoggedInUser: rp.oauth.GetMultiAccountUser(r),
888
888
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
889
889
Issues: issues,
890
890
IssueCount: totalIssues,
···
897
897
898
898
func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) {
899
899
l := rp.logger.With("handler", "NewIssue")
900
-
user := rp.oauth.GetUser(r)
900
+
user := rp.oauth.GetMultiAccountUser(r)
901
901
902
902
f, err := rp.repoResolver.Resolve(r)
903
903
if err != nil {
···
921
921
Title: r.FormValue("title"),
922
922
Body: body,
923
923
Open: true,
924
-
Did: user.Did,
924
+
Did: user.Active.Did,
925
925
Created: time.Now(),
926
926
Mentions: mentions,
927
927
References: references,
···
945
945
}
946
946
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
947
947
Collection: tangled.RepoIssueNSID,
948
-
Repo: user.Did,
948
+
Repo: user.Active.Did,
949
949
Rkey: issue.Rkey,
950
950
Record: &lexutil.LexiconTypeDecoder{
951
951
Val: &record,
+31
-36
appview/knots/knots.go
+31
-36
appview/knots/knots.go
···
70
70
}
71
71
72
72
func (k *Knots) knots(w http.ResponseWriter, r *http.Request) {
73
-
user := k.OAuth.GetUser(r)
73
+
user := k.OAuth.GetMultiAccountUser(r)
74
74
registrations, err := db.GetRegistrations(
75
75
k.Db,
76
-
orm.FilterEq("did", user.Did),
76
+
orm.FilterEq("did", user.Active.Did),
77
77
)
78
78
if err != nil {
79
79
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
92
92
func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) {
93
93
l := k.Logger.With("handler", "dashboard")
94
94
95
-
user := k.OAuth.GetUser(r)
96
-
l = l.With("user", user.Did)
95
+
user := k.OAuth.GetMultiAccountUser(r)
96
+
l = l.With("user", user.Active.Did)
97
97
98
98
domain := chi.URLParam(r, "domain")
99
99
if domain == "" {
···
103
103
104
104
registrations, err := db.GetRegistrations(
105
105
k.Db,
106
-
orm.FilterEq("did", user.Did),
106
+
orm.FilterEq("did", user.Active.Did),
107
107
orm.FilterEq("domain", domain),
108
108
)
109
109
if err != nil {
···
154
154
}
155
155
156
156
func (k *Knots) register(w http.ResponseWriter, r *http.Request) {
157
-
user := k.OAuth.GetUser(r)
157
+
user := k.OAuth.GetMultiAccountUser(r)
158
158
l := k.Logger.With("handler", "register")
159
159
160
160
noticeId := "register-error"
···
175
175
return
176
176
}
177
177
l = l.With("domain", domain)
178
-
l = l.With("user", user.Did)
178
+
l = l.With("user", user.Active.Did)
179
179
180
180
tx, err := k.Db.Begin()
181
181
if err != nil {
···
188
188
k.Enforcer.E.LoadPolicy()
189
189
}()
190
190
191
-
err = db.AddKnot(tx, domain, user.Did)
191
+
err = db.AddKnot(tx, domain, user.Active.Did)
192
192
if err != nil {
193
193
l.Error("failed to insert", "err", err)
194
194
fail()
···
210
210
return
211
211
}
212
212
213
-
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain)
213
+
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain)
214
214
var exCid *string
215
215
if ex != nil {
216
216
exCid = ex.Cid
···
219
219
// re-announce by registering under same rkey
220
220
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
221
221
Collection: tangled.KnotNSID,
222
-
Repo: user.Did,
222
+
Repo: user.Active.Did,
223
223
Rkey: domain,
224
224
Record: &lexutil.LexiconTypeDecoder{
225
225
Val: &tangled.Knot{
···
250
250
}
251
251
252
252
// begin verification
253
-
err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
253
+
err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev)
254
254
if err != nil {
255
255
l.Error("verification failed", "err", err)
256
256
k.Pages.HxRefresh(w)
257
257
return
258
258
}
259
259
260
-
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
260
+
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did)
261
261
if err != nil {
262
262
l.Error("failed to mark verified", "err", err)
263
263
k.Pages.HxRefresh(w)
···
275
275
}
276
276
277
277
func (k *Knots) delete(w http.ResponseWriter, r *http.Request) {
278
-
user := k.OAuth.GetUser(r)
278
+
user := k.OAuth.GetMultiAccountUser(r)
279
279
l := k.Logger.With("handler", "delete")
280
280
281
281
noticeId := "operation-error"
···
294
294
// get record from db first
295
295
registrations, err := db.GetRegistrations(
296
296
k.Db,
297
-
orm.FilterEq("did", user.Did),
297
+
orm.FilterEq("did", user.Active.Did),
298
298
orm.FilterEq("domain", domain),
299
299
)
300
300
if err != nil {
···
322
322
323
323
err = db.DeleteKnot(
324
324
tx,
325
-
orm.FilterEq("did", user.Did),
325
+
orm.FilterEq("did", user.Active.Did),
326
326
orm.FilterEq("domain", domain),
327
327
)
328
328
if err != nil {
···
350
350
351
351
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
352
352
Collection: tangled.KnotNSID,
353
-
Repo: user.Did,
353
+
Repo: user.Active.Did,
354
354
Rkey: domain,
355
355
})
356
356
if err != nil {
···
382
382
}
383
383
384
384
func (k *Knots) retry(w http.ResponseWriter, r *http.Request) {
385
-
user := k.OAuth.GetUser(r)
385
+
user := k.OAuth.GetMultiAccountUser(r)
386
386
l := k.Logger.With("handler", "retry")
387
387
388
388
noticeId := "operation-error"
···
398
398
return
399
399
}
400
400
l = l.With("domain", domain)
401
-
l = l.With("user", user.Did)
401
+
l = l.With("user", user.Active.Did)
402
402
403
403
// get record from db first
404
404
registrations, err := db.GetRegistrations(
405
405
k.Db,
406
-
orm.FilterEq("did", user.Did),
406
+
orm.FilterEq("did", user.Active.Did),
407
407
orm.FilterEq("domain", domain),
408
408
)
409
409
if err != nil {
···
419
419
registration := registrations[0]
420
420
421
421
// begin verification
422
-
err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
422
+
err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev)
423
423
if err != nil {
424
424
l.Error("verification failed", "err", err)
425
425
···
437
437
return
438
438
}
439
439
440
-
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
440
+
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did)
441
441
if err != nil {
442
442
l.Error("failed to mark verified", "err", err)
443
443
k.Pages.Notice(w, noticeId, err.Error())
···
456
456
return
457
457
}
458
458
459
-
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain)
459
+
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain)
460
460
var exCid *string
461
461
if ex != nil {
462
462
exCid = ex.Cid
···
465
465
// ignore the error here
466
466
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
467
467
Collection: tangled.KnotNSID,
468
-
Repo: user.Did,
468
+
Repo: user.Active.Did,
469
469
Rkey: domain,
470
470
Record: &lexutil.LexiconTypeDecoder{
471
471
Val: &tangled.Knot{
···
494
494
// Get updated registration to show
495
495
registrations, err = db.GetRegistrations(
496
496
k.Db,
497
-
orm.FilterEq("did", user.Did),
497
+
orm.FilterEq("did", user.Active.Did),
498
498
orm.FilterEq("domain", domain),
499
499
)
500
500
if err != nil {
···
516
516
}
517
517
518
518
func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) {
519
-
user := k.OAuth.GetUser(r)
519
+
user := k.OAuth.GetMultiAccountUser(r)
520
520
l := k.Logger.With("handler", "addMember")
521
521
522
522
domain := chi.URLParam(r, "domain")
···
526
526
return
527
527
}
528
528
l = l.With("domain", domain)
529
-
l = l.With("user", user.Did)
529
+
l = l.With("user", user.Active.Did)
530
530
531
531
registrations, err := db.GetRegistrations(
532
532
k.Db,
533
-
orm.FilterEq("did", user.Did),
533
+
orm.FilterEq("did", user.Active.Did),
534
534
orm.FilterEq("domain", domain),
535
535
orm.FilterIsNot("registered", "null"),
536
536
)
···
583
583
584
584
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
585
585
Collection: tangled.KnotMemberNSID,
586
-
Repo: user.Did,
586
+
Repo: user.Active.Did,
587
587
Rkey: rkey,
588
588
Record: &lexutil.LexiconTypeDecoder{
589
589
Val: &tangled.KnotMember{
···
618
618
}
619
619
620
620
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
621
-
user := k.OAuth.GetUser(r)
621
+
user := k.OAuth.GetMultiAccountUser(r)
622
622
l := k.Logger.With("handler", "removeMember")
623
623
624
624
noticeId := "operation-error"
···
634
634
return
635
635
}
636
636
l = l.With("domain", domain)
637
-
l = l.With("user", user.Did)
637
+
l = l.With("user", user.Active.Did)
638
638
639
639
registrations, err := db.GetRegistrations(
640
640
k.Db,
641
-
orm.FilterEq("did", user.Did),
641
+
orm.FilterEq("did", user.Active.Did),
642
642
orm.FilterEq("domain", domain),
643
643
orm.FilterIsNot("registered", "null"),
644
644
)
···
663
663
memberId, err := k.IdResolver.ResolveIdent(r.Context(), member)
664
664
if err != nil {
665
665
l.Error("failed to resolve member identity to handle", "err", err)
666
-
k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
667
-
return
668
-
}
669
-
if memberId.Handle.IsInvalidHandle() {
670
-
l.Error("failed to resolve member identity to handle")
671
666
k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
672
667
return
673
668
}
+2
-2
appview/labels/labels.go
+2
-2
appview/labels/labels.go
···
68
68
// - this handler should calculate the diff in order to create the labelop record
69
69
// - we need the diff in order to maintain a "history" of operations performed by users
70
70
func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) {
71
-
user := l.oauth.GetUser(r)
71
+
user := l.oauth.GetMultiAccountUser(r)
72
72
73
73
noticeId := "add-label-error"
74
74
···
82
82
return
83
83
}
84
84
85
-
did := user.Did
85
+
did := user.Active.Did
86
86
rkey := tid.TID()
87
87
performedAt := time.Now()
88
88
indexedAt := time.Now()
+10
-8
appview/middleware/middleware.go
+10
-8
appview/middleware/middleware.go
···
115
115
return func(next http.Handler) http.Handler {
116
116
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117
117
// requires auth also
118
-
actor := mw.oauth.GetUser(r)
118
+
actor := mw.oauth.GetMultiAccountUser(r)
119
119
if actor == nil {
120
120
// we need a logged in user
121
121
log.Printf("not logged in, redirecting")
···
128
128
return
129
129
}
130
130
131
-
ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
131
+
ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Active.Did, group, domain)
132
132
if err != nil || !ok {
133
-
// we need a logged in user
134
-
log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain)
133
+
log.Printf("%s does not have perms of a %s in domain %s", actor.Active.Did, group, domain)
135
134
http.Error(w, "Forbiden", http.StatusUnauthorized)
136
135
return
137
136
}
···
149
148
return func(next http.Handler) http.Handler {
150
149
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151
150
// requires auth also
152
-
actor := mw.oauth.GetUser(r)
151
+
actor := mw.oauth.GetMultiAccountUser(r)
153
152
if actor == nil {
154
153
// we need a logged in user
155
154
log.Printf("not logged in, redirecting")
···
162
161
return
163
162
}
164
163
165
-
ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
164
+
ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
166
165
if err != nil || !ok {
167
-
// we need a logged in user
168
-
log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo())
166
+
log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo())
169
167
http.Error(w, "Forbiden", http.StatusUnauthorized)
170
168
return
171
169
}
···
223
221
)
224
222
if err != nil {
225
223
log.Println("failed to resolve repo", "err", err)
224
+
w.WriteHeader(http.StatusNotFound)
226
225
mw.pages.ErrorKnot404(w)
227
226
return
228
227
}
···
240
239
f, err := mw.repoResolver.Resolve(r)
241
240
if err != nil {
242
241
log.Println("failed to fully resolve repo", err)
242
+
w.WriteHeader(http.StatusNotFound)
243
243
mw.pages.ErrorKnot404(w)
244
244
return
245
245
}
···
288
288
f, err := mw.repoResolver.Resolve(r)
289
289
if err != nil {
290
290
log.Println("failed to fully resolve repo", err)
291
+
w.WriteHeader(http.StatusNotFound)
291
292
mw.pages.ErrorKnot404(w)
292
293
return
293
294
}
···
324
325
f, err := mw.repoResolver.Resolve(r)
325
326
if err != nil {
326
327
log.Println("failed to fully resolve repo", err)
328
+
w.WriteHeader(http.StatusNotFound)
327
329
mw.pages.ErrorKnot404(w)
328
330
return
329
331
}
+1
-1
appview/models/pull.go
+1
-1
appview/models/pull.go
···
83
83
Repo *Repo
84
84
}
85
85
86
+
// NOTE: This method does not include patch blob in returned atproto record
86
87
func (p Pull) AsRecord() tangled.RepoPull {
87
88
var source *tangled.RepoPull_Source
88
89
if p.PullSource != nil {
···
113
114
Repo: p.RepoAt.String(),
114
115
Branch: p.TargetBranch,
115
116
},
116
-
Patch: p.LatestPatch(),
117
117
Source: source,
118
118
}
119
119
return record
+6
-6
appview/notifications/notifications.go
+6
-6
appview/notifications/notifications.go
···
48
48
49
49
func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) {
50
50
l := n.logger.With("handler", "notificationsPage")
51
-
user := n.oauth.GetUser(r)
51
+
user := n.oauth.GetMultiAccountUser(r)
52
52
53
53
page := pagination.FromContext(r.Context())
54
54
55
55
total, err := db.CountNotifications(
56
56
n.db,
57
-
orm.FilterEq("recipient_did", user.Did),
57
+
orm.FilterEq("recipient_did", user.Active.Did),
58
58
)
59
59
if err != nil {
60
60
l.Error("failed to get total notifications", "err", err)
···
65
65
notifications, err := db.GetNotificationsWithEntities(
66
66
n.db,
67
67
page,
68
-
orm.FilterEq("recipient_did", user.Did),
68
+
orm.FilterEq("recipient_did", user.Active.Did),
69
69
)
70
70
if err != nil {
71
71
l.Error("failed to get notifications", "err", err)
···
73
73
return
74
74
}
75
75
76
-
err = db.MarkAllNotificationsRead(n.db, user.Did)
76
+
err = db.MarkAllNotificationsRead(n.db, user.Active.Did)
77
77
if err != nil {
78
78
l.Error("failed to mark notifications as read", "err", err)
79
79
}
···
90
90
}
91
91
92
92
func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) {
93
-
user := n.oauth.GetUser(r)
93
+
user := n.oauth.GetMultiAccountUser(r)
94
94
if user == nil {
95
95
return
96
96
}
97
97
98
98
count, err := db.CountNotifications(
99
99
n.db,
100
-
orm.FilterEq("recipient_did", user.Did),
100
+
orm.FilterEq("recipient_did", user.Active.Did),
101
101
orm.FilterEq("read", 0),
102
102
)
103
103
if err != nil {
+191
appview/oauth/accounts.go
+191
appview/oauth/accounts.go
···
1
+
package oauth
2
+
3
+
import (
4
+
"encoding/json"
5
+
"errors"
6
+
"net/http"
7
+
"time"
8
+
)
9
+
10
+
const MaxAccounts = 20
11
+
12
+
var ErrMaxAccountsReached = errors.New("maximum number of linked accounts reached")
13
+
14
+
type AccountInfo struct {
15
+
Did string `json:"did"`
16
+
Handle string `json:"handle"`
17
+
SessionId string `json:"session_id"`
18
+
AddedAt int64 `json:"added_at"`
19
+
}
20
+
21
+
type AccountRegistry struct {
22
+
Accounts []AccountInfo `json:"accounts"`
23
+
}
24
+
25
+
type MultiAccountUser struct {
26
+
Active *User
27
+
Accounts []AccountInfo
28
+
}
29
+
30
+
func (m *MultiAccountUser) Did() string {
31
+
if m.Active == nil {
32
+
return ""
33
+
}
34
+
return m.Active.Did
35
+
}
36
+
37
+
func (m *MultiAccountUser) Pds() string {
38
+
if m.Active == nil {
39
+
return ""
40
+
}
41
+
return m.Active.Pds
42
+
}
43
+
44
+
func (o *OAuth) GetAccounts(r *http.Request) *AccountRegistry {
45
+
session, err := o.SessStore.Get(r, AccountsName)
46
+
if err != nil || session.IsNew {
47
+
return &AccountRegistry{Accounts: []AccountInfo{}}
48
+
}
49
+
50
+
data, ok := session.Values["accounts"].(string)
51
+
if !ok {
52
+
return &AccountRegistry{Accounts: []AccountInfo{}}
53
+
}
54
+
55
+
var registry AccountRegistry
56
+
if err := json.Unmarshal([]byte(data), ®istry); err != nil {
57
+
return &AccountRegistry{Accounts: []AccountInfo{}}
58
+
}
59
+
60
+
return ®istry
61
+
}
62
+
63
+
func (o *OAuth) SaveAccounts(w http.ResponseWriter, r *http.Request, registry *AccountRegistry) error {
64
+
session, err := o.SessStore.Get(r, AccountsName)
65
+
if err != nil {
66
+
return err
67
+
}
68
+
69
+
data, err := json.Marshal(registry)
70
+
if err != nil {
71
+
return err
72
+
}
73
+
74
+
session.Values["accounts"] = string(data)
75
+
session.Options.MaxAge = 60 * 60 * 24 * 365
76
+
session.Options.HttpOnly = true
77
+
session.Options.Secure = !o.Config.Core.Dev
78
+
session.Options.SameSite = http.SameSiteLaxMode
79
+
80
+
return session.Save(r, w)
81
+
}
82
+
83
+
func (r *AccountRegistry) AddAccount(did, handle, sessionId string) error {
84
+
for i, acc := range r.Accounts {
85
+
if acc.Did == did {
86
+
r.Accounts[i].SessionId = sessionId
87
+
r.Accounts[i].Handle = handle
88
+
return nil
89
+
}
90
+
}
91
+
92
+
if len(r.Accounts) >= MaxAccounts {
93
+
return ErrMaxAccountsReached
94
+
}
95
+
96
+
r.Accounts = append(r.Accounts, AccountInfo{
97
+
Did: did,
98
+
Handle: handle,
99
+
SessionId: sessionId,
100
+
AddedAt: time.Now().Unix(),
101
+
})
102
+
return nil
103
+
}
104
+
105
+
func (r *AccountRegistry) RemoveAccount(did string) {
106
+
filtered := make([]AccountInfo, 0, len(r.Accounts))
107
+
for _, acc := range r.Accounts {
108
+
if acc.Did != did {
109
+
filtered = append(filtered, acc)
110
+
}
111
+
}
112
+
r.Accounts = filtered
113
+
}
114
+
115
+
func (r *AccountRegistry) FindAccount(did string) *AccountInfo {
116
+
for i := range r.Accounts {
117
+
if r.Accounts[i].Did == did {
118
+
return &r.Accounts[i]
119
+
}
120
+
}
121
+
return nil
122
+
}
123
+
124
+
func (r *AccountRegistry) OtherAccounts(activeDid string) []AccountInfo {
125
+
result := make([]AccountInfo, 0, len(r.Accounts))
126
+
for _, acc := range r.Accounts {
127
+
if acc.Did != activeDid {
128
+
result = append(result, acc)
129
+
}
130
+
}
131
+
return result
132
+
}
133
+
134
+
func (o *OAuth) GetMultiAccountUser(r *http.Request) *MultiAccountUser {
135
+
user := o.GetUser(r)
136
+
if user == nil {
137
+
return nil
138
+
}
139
+
140
+
registry := o.GetAccounts(r)
141
+
return &MultiAccountUser{
142
+
Active: user,
143
+
Accounts: registry.Accounts,
144
+
}
145
+
}
146
+
147
+
type AuthReturnInfo struct {
148
+
ReturnURL string
149
+
AddAccount bool
150
+
}
151
+
152
+
func (o *OAuth) SetAuthReturn(w http.ResponseWriter, r *http.Request, returnURL string, addAccount bool) error {
153
+
session, err := o.SessStore.Get(r, AuthReturnName)
154
+
if err != nil {
155
+
return err
156
+
}
157
+
158
+
session.Values[AuthReturnURL] = returnURL
159
+
session.Values[AuthAddAccount] = addAccount
160
+
session.Options.MaxAge = 60 * 30
161
+
session.Options.HttpOnly = true
162
+
session.Options.Secure = !o.Config.Core.Dev
163
+
session.Options.SameSite = http.SameSiteLaxMode
164
+
165
+
return session.Save(r, w)
166
+
}
167
+
168
+
func (o *OAuth) GetAuthReturn(r *http.Request) *AuthReturnInfo {
169
+
session, err := o.SessStore.Get(r, AuthReturnName)
170
+
if err != nil || session.IsNew {
171
+
return &AuthReturnInfo{}
172
+
}
173
+
174
+
returnURL, _ := session.Values[AuthReturnURL].(string)
175
+
addAccount, _ := session.Values[AuthAddAccount].(bool)
176
+
177
+
return &AuthReturnInfo{
178
+
ReturnURL: returnURL,
179
+
AddAccount: addAccount,
180
+
}
181
+
}
182
+
183
+
func (o *OAuth) ClearAuthReturn(w http.ResponseWriter, r *http.Request) error {
184
+
session, err := o.SessStore.Get(r, AuthReturnName)
185
+
if err != nil {
186
+
return err
187
+
}
188
+
189
+
session.Options.MaxAge = -1
190
+
return session.Save(r, w)
191
+
}
+265
appview/oauth/accounts_test.go
+265
appview/oauth/accounts_test.go
···
1
+
package oauth
2
+
3
+
import (
4
+
"testing"
5
+
)
6
+
7
+
func TestAccountRegistry_AddAccount(t *testing.T) {
8
+
tests := []struct {
9
+
name string
10
+
initial []AccountInfo
11
+
addDid string
12
+
addHandle string
13
+
addSessionId string
14
+
wantErr error
15
+
wantLen int
16
+
wantSessionId string
17
+
}{
18
+
{
19
+
name: "add first account",
20
+
initial: []AccountInfo{},
21
+
addDid: "did:plc:abc123",
22
+
addHandle: "alice.bsky.social",
23
+
addSessionId: "session-1",
24
+
wantErr: nil,
25
+
wantLen: 1,
26
+
wantSessionId: "session-1",
27
+
},
28
+
{
29
+
name: "add second account",
30
+
initial: []AccountInfo{
31
+
{Did: "did:plc:abc123", Handle: "alice.bsky.social", SessionId: "session-1", AddedAt: 1000},
32
+
},
33
+
addDid: "did:plc:def456",
34
+
addHandle: "bob.bsky.social",
35
+
addSessionId: "session-2",
36
+
wantErr: nil,
37
+
wantLen: 2,
38
+
wantSessionId: "session-2",
39
+
},
40
+
{
41
+
name: "update existing account session",
42
+
initial: []AccountInfo{
43
+
{Did: "did:plc:abc123", Handle: "alice.bsky.social", SessionId: "old-session", AddedAt: 1000},
44
+
},
45
+
addDid: "did:plc:abc123",
46
+
addHandle: "alice.bsky.social",
47
+
addSessionId: "new-session",
48
+
wantErr: nil,
49
+
wantLen: 1,
50
+
wantSessionId: "new-session",
51
+
},
52
+
}
53
+
54
+
for _, tt := range tests {
55
+
t.Run(tt.name, func(t *testing.T) {
56
+
registry := &AccountRegistry{Accounts: tt.initial}
57
+
err := registry.AddAccount(tt.addDid, tt.addHandle, tt.addSessionId)
58
+
59
+
if err != tt.wantErr {
60
+
t.Errorf("AddAccount() error = %v, want %v", err, tt.wantErr)
61
+
}
62
+
63
+
if len(registry.Accounts) != tt.wantLen {
64
+
t.Errorf("AddAccount() len = %d, want %d", len(registry.Accounts), tt.wantLen)
65
+
}
66
+
67
+
found := registry.FindAccount(tt.addDid)
68
+
if found == nil {
69
+
t.Errorf("AddAccount() account not found after add")
70
+
return
71
+
}
72
+
73
+
if found.SessionId != tt.wantSessionId {
74
+
t.Errorf("AddAccount() sessionId = %s, want %s", found.SessionId, tt.wantSessionId)
75
+
}
76
+
})
77
+
}
78
+
}
79
+
80
+
func TestAccountRegistry_AddAccount_MaxLimit(t *testing.T) {
81
+
registry := &AccountRegistry{Accounts: make([]AccountInfo, 0, MaxAccounts)}
82
+
83
+
for i := range MaxAccounts {
84
+
err := registry.AddAccount("did:plc:user"+string(rune('a'+i)), "handle", "session")
85
+
if err != nil {
86
+
t.Fatalf("AddAccount() unexpected error on account %d: %v", i, err)
87
+
}
88
+
}
89
+
90
+
if len(registry.Accounts) != MaxAccounts {
91
+
t.Errorf("expected %d accounts, got %d", MaxAccounts, len(registry.Accounts))
92
+
}
93
+
94
+
err := registry.AddAccount("did:plc:overflow", "overflow", "session-overflow")
95
+
if err != ErrMaxAccountsReached {
96
+
t.Errorf("AddAccount() error = %v, want %v", err, ErrMaxAccountsReached)
97
+
}
98
+
99
+
if len(registry.Accounts) != MaxAccounts {
100
+
t.Errorf("account added despite max limit, got %d", len(registry.Accounts))
101
+
}
102
+
}
103
+
104
+
func TestAccountRegistry_RemoveAccount(t *testing.T) {
105
+
tests := []struct {
106
+
name string
107
+
initial []AccountInfo
108
+
removeDid string
109
+
wantLen int
110
+
wantDids []string
111
+
}{
112
+
{
113
+
name: "remove existing account",
114
+
initial: []AccountInfo{
115
+
{Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"},
116
+
{Did: "did:plc:def456", Handle: "bob", SessionId: "s2"},
117
+
},
118
+
removeDid: "did:plc:abc123",
119
+
wantLen: 1,
120
+
wantDids: []string{"did:plc:def456"},
121
+
},
122
+
{
123
+
name: "remove non-existing account",
124
+
initial: []AccountInfo{
125
+
{Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"},
126
+
},
127
+
removeDid: "did:plc:notfound",
128
+
wantLen: 1,
129
+
wantDids: []string{"did:plc:abc123"},
130
+
},
131
+
{
132
+
name: "remove last account",
133
+
initial: []AccountInfo{
134
+
{Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"},
135
+
},
136
+
removeDid: "did:plc:abc123",
137
+
wantLen: 0,
138
+
wantDids: []string{},
139
+
},
140
+
{
141
+
name: "remove from empty registry",
142
+
initial: []AccountInfo{},
143
+
removeDid: "did:plc:abc123",
144
+
wantLen: 0,
145
+
wantDids: []string{},
146
+
},
147
+
}
148
+
149
+
for _, tt := range tests {
150
+
t.Run(tt.name, func(t *testing.T) {
151
+
registry := &AccountRegistry{Accounts: tt.initial}
152
+
registry.RemoveAccount(tt.removeDid)
153
+
154
+
if len(registry.Accounts) != tt.wantLen {
155
+
t.Errorf("RemoveAccount() len = %d, want %d", len(registry.Accounts), tt.wantLen)
156
+
}
157
+
158
+
for _, wantDid := range tt.wantDids {
159
+
if registry.FindAccount(wantDid) == nil {
160
+
t.Errorf("RemoveAccount() expected %s to remain", wantDid)
161
+
}
162
+
}
163
+
164
+
if registry.FindAccount(tt.removeDid) != nil && tt.wantLen < len(tt.initial) {
165
+
t.Errorf("RemoveAccount() %s should have been removed", tt.removeDid)
166
+
}
167
+
})
168
+
}
169
+
}
170
+
171
+
func TestAccountRegistry_FindAccount(t *testing.T) {
172
+
registry := &AccountRegistry{
173
+
Accounts: []AccountInfo{
174
+
{Did: "did:plc:first", Handle: "first", SessionId: "s1", AddedAt: 1000},
175
+
{Did: "did:plc:second", Handle: "second", SessionId: "s2", AddedAt: 2000},
176
+
{Did: "did:plc:third", Handle: "third", SessionId: "s3", AddedAt: 3000},
177
+
},
178
+
}
179
+
180
+
t.Run("find existing account", func(t *testing.T) {
181
+
found := registry.FindAccount("did:plc:second")
182
+
if found == nil {
183
+
t.Fatal("FindAccount() returned nil for existing account")
184
+
}
185
+
if found.Handle != "second" {
186
+
t.Errorf("FindAccount() handle = %s, want second", found.Handle)
187
+
}
188
+
if found.SessionId != "s2" {
189
+
t.Errorf("FindAccount() sessionId = %s, want s2", found.SessionId)
190
+
}
191
+
})
192
+
193
+
t.Run("find non-existing account", func(t *testing.T) {
194
+
found := registry.FindAccount("did:plc:notfound")
195
+
if found != nil {
196
+
t.Errorf("FindAccount() = %v, want nil", found)
197
+
}
198
+
})
199
+
200
+
t.Run("returned pointer is mutable", func(t *testing.T) {
201
+
found := registry.FindAccount("did:plc:first")
202
+
if found == nil {
203
+
t.Fatal("FindAccount() returned nil")
204
+
}
205
+
found.SessionId = "modified"
206
+
207
+
refetch := registry.FindAccount("did:plc:first")
208
+
if refetch.SessionId != "modified" {
209
+
t.Errorf("FindAccount() pointer not referencing original, got %s", refetch.SessionId)
210
+
}
211
+
})
212
+
}
213
+
214
+
func TestAccountRegistry_OtherAccounts(t *testing.T) {
215
+
registry := &AccountRegistry{
216
+
Accounts: []AccountInfo{
217
+
{Did: "did:plc:active", Handle: "active", SessionId: "s1"},
218
+
{Did: "did:plc:other1", Handle: "other1", SessionId: "s2"},
219
+
{Did: "did:plc:other2", Handle: "other2", SessionId: "s3"},
220
+
},
221
+
}
222
+
223
+
others := registry.OtherAccounts("did:plc:active")
224
+
225
+
if len(others) != 2 {
226
+
t.Errorf("OtherAccounts() len = %d, want 2", len(others))
227
+
}
228
+
229
+
for _, acc := range others {
230
+
if acc.Did == "did:plc:active" {
231
+
t.Errorf("OtherAccounts() should not include active account")
232
+
}
233
+
}
234
+
235
+
hasDid := func(did string) bool {
236
+
for _, acc := range others {
237
+
if acc.Did == did {
238
+
return true
239
+
}
240
+
}
241
+
return false
242
+
}
243
+
244
+
if !hasDid("did:plc:other1") || !hasDid("did:plc:other2") {
245
+
t.Errorf("OtherAccounts() missing expected accounts")
246
+
}
247
+
}
248
+
249
+
func TestMultiAccountUser_Did(t *testing.T) {
250
+
t.Run("with active user", func(t *testing.T) {
251
+
user := &MultiAccountUser{
252
+
Active: &User{Did: "did:plc:test", Pds: "https://bsky.social"},
253
+
}
254
+
if user.Did() != "did:plc:test" {
255
+
t.Errorf("Did() = %s, want did:plc:test", user.Did())
256
+
}
257
+
})
258
+
259
+
t.Run("with nil active", func(t *testing.T) {
260
+
user := &MultiAccountUser{Active: nil}
261
+
if user.Did() != "" {
262
+
t.Errorf("Did() = %s, want empty string", user.Did())
263
+
}
264
+
})
265
+
}
+5
-1
appview/oauth/consts.go
+5
-1
appview/oauth/consts.go
···
1
1
package oauth
2
2
3
3
const (
4
-
SessionName = "appview-session-v2"
4
+
SessionName = "appview-session-v2"
5
+
AccountsName = "appview-accounts-v2"
6
+
AuthReturnName = "appview-auth-return"
7
+
AuthReturnURL = "return_url"
8
+
AuthAddAccount = "add_account"
5
9
SessionHandle = "handle"
6
10
SessionDid = "did"
7
11
SessionId = "id"
+14
-2
appview/oauth/handler.go
+14
-2
appview/oauth/handler.go
···
55
55
ctx := r.Context()
56
56
l := o.Logger.With("query", r.URL.Query())
57
57
58
+
authReturn := o.GetAuthReturn(r)
59
+
_ = o.ClearAuthReturn(w, r)
60
+
58
61
sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query())
59
62
if err != nil {
60
63
var callbackErr *oauth.AuthRequestCallbackError
···
70
73
71
74
if err := o.SaveSession(w, r, sessData); err != nil {
72
75
l.Error("failed to save session", "data", sessData, "err", err)
73
-
http.Redirect(w, r, "/login?error=session", http.StatusFound)
76
+
errorCode := "session"
77
+
if errors.Is(err, ErrMaxAccountsReached) {
78
+
errorCode = "max_accounts"
79
+
}
80
+
http.Redirect(w, r, fmt.Sprintf("/login?error=%s", errorCode), http.StatusFound)
74
81
return
75
82
}
76
83
···
88
95
}
89
96
}
90
97
91
-
http.Redirect(w, r, "/", http.StatusFound)
98
+
redirectURL := "/"
99
+
if authReturn.ReturnURL != "" {
100
+
redirectURL = authReturn.ReturnURL
101
+
}
102
+
103
+
http.Redirect(w, r, redirectURL, http.StatusFound)
92
104
}
93
105
94
106
func (o *OAuth) addToDefaultSpindle(did string) {
+66
-4
appview/oauth/oauth.go
+66
-4
appview/oauth/oauth.go
···
98
98
}
99
99
100
100
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error {
101
-
// first we save the did in the user session
102
101
userSession, err := o.SessStore.Get(r, SessionName)
103
102
if err != nil {
104
103
return err
···
108
107
userSession.Values[SessionPds] = sessData.HostURL
109
108
userSession.Values[SessionId] = sessData.SessionID
110
109
userSession.Values[SessionAuthenticated] = true
111
-
return userSession.Save(r, w)
110
+
111
+
if err := userSession.Save(r, w); err != nil {
112
+
return err
113
+
}
114
+
115
+
handle := ""
116
+
resolved, err := o.IdResolver.ResolveIdent(r.Context(), sessData.AccountDID.String())
117
+
if err == nil && resolved.Handle.String() != "" {
118
+
handle = resolved.Handle.String()
119
+
}
120
+
121
+
registry := o.GetAccounts(r)
122
+
if err := registry.AddAccount(sessData.AccountDID.String(), handle, sessData.SessionID); err != nil {
123
+
return err
124
+
}
125
+
return o.SaveAccounts(w, r, registry)
112
126
}
113
127
114
128
func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) {
···
163
177
return errors.Join(err1, err2)
164
178
}
165
179
180
+
func (o *OAuth) SwitchAccount(w http.ResponseWriter, r *http.Request, targetDid string) error {
181
+
registry := o.GetAccounts(r)
182
+
account := registry.FindAccount(targetDid)
183
+
if account == nil {
184
+
return fmt.Errorf("account not found in registry: %s", targetDid)
185
+
}
186
+
187
+
did, err := syntax.ParseDID(targetDid)
188
+
if err != nil {
189
+
return fmt.Errorf("invalid DID: %w", err)
190
+
}
191
+
192
+
sess, err := o.ClientApp.ResumeSession(r.Context(), did, account.SessionId)
193
+
if err != nil {
194
+
registry.RemoveAccount(targetDid)
195
+
_ = o.SaveAccounts(w, r, registry)
196
+
return fmt.Errorf("session expired for account: %w", err)
197
+
}
198
+
199
+
userSession, err := o.SessStore.Get(r, SessionName)
200
+
if err != nil {
201
+
return err
202
+
}
203
+
204
+
userSession.Values[SessionDid] = sess.Data.AccountDID.String()
205
+
userSession.Values[SessionPds] = sess.Data.HostURL
206
+
userSession.Values[SessionId] = sess.Data.SessionID
207
+
userSession.Values[SessionAuthenticated] = true
208
+
209
+
return userSession.Save(r, w)
210
+
}
211
+
212
+
func (o *OAuth) RemoveAccount(w http.ResponseWriter, r *http.Request, targetDid string) error {
213
+
registry := o.GetAccounts(r)
214
+
account := registry.FindAccount(targetDid)
215
+
if account == nil {
216
+
return nil
217
+
}
218
+
219
+
did, err := syntax.ParseDID(targetDid)
220
+
if err == nil {
221
+
_ = o.ClientApp.Logout(r.Context(), did, account.SessionId)
222
+
}
223
+
224
+
registry.RemoveAccount(targetDid)
225
+
return o.SaveAccounts(w, r, registry)
226
+
}
227
+
166
228
type User struct {
167
229
Did string
168
230
Pds string
···
181
243
}
182
244
183
245
func (o *OAuth) GetDid(r *http.Request) string {
184
-
if u := o.GetUser(r); u != nil {
185
-
return u.Did
246
+
if u := o.GetMultiAccountUser(r); u != nil {
247
+
return u.Did()
186
248
}
187
249
188
250
return ""
+10
appview/pages/funcmap.go
+10
appview/pages/funcmap.go
···
28
28
emoji "github.com/yuin/goldmark-emoji"
29
29
"tangled.org/core/appview/filetree"
30
30
"tangled.org/core/appview/models"
31
+
"tangled.org/core/appview/oauth"
31
32
"tangled.org/core/appview/pages/markup"
32
33
"tangled.org/core/crypto"
33
34
)
···
384
385
return "error"
385
386
}
386
387
return fp
388
+
},
389
+
"otherAccounts": func(activeDid string, accounts []oauth.AccountInfo) []oauth.AccountInfo {
390
+
result := make([]oauth.AccountInfo, 0, len(accounts))
391
+
for _, acc := range accounts {
392
+
if acc.Did != activeDid {
393
+
result = append(result, acc)
394
+
}
395
+
}
396
+
return result
387
397
},
388
398
}
389
399
}
+13
-3
appview/pages/markup/extension/atlink.go
+13
-3
appview/pages/markup/extension/atlink.go
···
35
35
return KindAt
36
36
}
37
37
38
-
var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)`)
38
+
var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\b)`)
39
+
var markdownLinkRegexp = regexp.MustCompile(`(?ms)\[.*\]\(.*\)`)
39
40
40
41
type atParser struct{}
41
42
···
55
56
if m == nil {
56
57
return nil
57
58
}
59
+
60
+
// Check for all links in the markdown to see if the handle found is inside one
61
+
linksIndexes := markdownLinkRegexp.FindAllIndex(block.Source(), -1)
62
+
for _, linkMatch := range linksIndexes {
63
+
if linkMatch[0] < segment.Start && segment.Start < linkMatch[1] {
64
+
return nil
65
+
}
66
+
}
67
+
58
68
atSegment := text.NewSegment(segment.Start, segment.Start+m[1])
59
69
block.Advance(m[1])
60
70
node := &AtNode{}
···
87
97
88
98
func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
89
99
if entering {
90
-
w.WriteString(`<a href="/@`)
100
+
w.WriteString(`<a href="/`)
91
101
w.WriteString(n.(*AtNode).Handle)
92
-
w.WriteString(`" class="mention font-bold">`)
102
+
w.WriteString(`" class="mention">`)
93
103
} else {
94
104
w.WriteString("</a>")
95
105
}
+121
appview/pages/markup/markdown_test.go
+121
appview/pages/markup/markdown_test.go
···
1
+
package markup
2
+
3
+
import (
4
+
"bytes"
5
+
"testing"
6
+
)
7
+
8
+
func TestAtExtension_Rendering(t *testing.T) {
9
+
tests := []struct {
10
+
name string
11
+
markdown string
12
+
expected string
13
+
}{
14
+
{
15
+
name: "renders simple at mention",
16
+
markdown: "Hello @user.tngl.sh!",
17
+
expected: `<p>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>!</p>`,
18
+
},
19
+
{
20
+
name: "renders multiple at mentions",
21
+
markdown: "Hi @alice.tngl.sh and @bob.example.com",
22
+
expected: `<p>Hi <a href="/alice.tngl.sh" class="mention">@alice.tngl.sh</a> and <a href="/bob.example.com" class="mention">@bob.example.com</a></p>`,
23
+
},
24
+
{
25
+
name: "renders at mention in parentheses",
26
+
markdown: "Check this out (@user.tngl.sh)",
27
+
expected: `<p>Check this out (<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>)</p>`,
28
+
},
29
+
{
30
+
name: "does not render email",
31
+
markdown: "Contact me at test@example.com",
32
+
expected: `<p>Contact me at <a href="mailto:test@example.com">test@example.com</a></p>`,
33
+
},
34
+
{
35
+
name: "renders at mention with hyphen",
36
+
markdown: "Follow @user-name.tngl.sh",
37
+
expected: `<p>Follow <a href="/user-name.tngl.sh" class="mention">@user-name.tngl.sh</a></p>`,
38
+
},
39
+
{
40
+
name: "renders at mention with numbers",
41
+
markdown: "@user123.test456.social",
42
+
expected: `<p><a href="/user123.test456.social" class="mention">@user123.test456.social</a></p>`,
43
+
},
44
+
{
45
+
name: "at mention at start of line",
46
+
markdown: "@user.tngl.sh is cool",
47
+
expected: `<p><a href="/user.tngl.sh" class="mention">@user.tngl.sh</a> is cool</p>`,
48
+
},
49
+
}
50
+
51
+
for _, tt := range tests {
52
+
t.Run(tt.name, func(t *testing.T) {
53
+
md := NewMarkdown()
54
+
55
+
var buf bytes.Buffer
56
+
if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
57
+
t.Fatalf("failed to convert markdown: %v", err)
58
+
}
59
+
60
+
result := buf.String()
61
+
if result != tt.expected+"\n" {
62
+
t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, result)
63
+
}
64
+
})
65
+
}
66
+
}
67
+
68
+
func TestAtExtension_WithOtherMarkdown(t *testing.T) {
69
+
tests := []struct {
70
+
name string
71
+
markdown string
72
+
contains string
73
+
}{
74
+
{
75
+
name: "at mention with bold",
76
+
markdown: "**Hello @user.tngl.sh**",
77
+
contains: `<strong>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></strong>`,
78
+
},
79
+
{
80
+
name: "at mention with italic",
81
+
markdown: "*Check @user.tngl.sh*",
82
+
contains: `<em>Check <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></em>`,
83
+
},
84
+
{
85
+
name: "at mention in list",
86
+
markdown: "- Item 1\n- @user.tngl.sh\n- Item 3",
87
+
contains: `<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>`,
88
+
},
89
+
{
90
+
name: "at mention in link",
91
+
markdown: "[@regnault.dev](https://regnault.dev)",
92
+
contains: `<a href="https://regnault.dev">@regnault.dev</a>`,
93
+
},
94
+
{
95
+
name: "at mention in link again",
96
+
markdown: "[check out @regnault.dev](https://regnault.dev)",
97
+
contains: `<a href="https://regnault.dev">check out @regnault.dev</a>`,
98
+
},
99
+
{
100
+
name: "at mention in link again, multiline",
101
+
markdown: "[\ncheck out @regnault.dev](https://regnault.dev)",
102
+
contains: "<a href=\"https://regnault.dev\">\ncheck out @regnault.dev</a>",
103
+
},
104
+
}
105
+
106
+
for _, tt := range tests {
107
+
t.Run(tt.name, func(t *testing.T) {
108
+
md := NewMarkdown()
109
+
110
+
var buf bytes.Buffer
111
+
if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
112
+
t.Fatalf("failed to convert markdown: %v", err)
113
+
}
114
+
115
+
result := buf.String()
116
+
if !bytes.Contains([]byte(result), []byte(tt.contains)) {
117
+
t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
118
+
}
119
+
})
120
+
}
121
+
}
+68
-66
appview/pages/pages.go
+68
-66
appview/pages/pages.go
···
215
215
}
216
216
217
217
type LoginParams struct {
218
-
ReturnUrl string
219
-
ErrorCode string
218
+
ReturnUrl string
219
+
ErrorCode string
220
+
AddAccount bool
221
+
LoggedInUser *oauth.MultiAccountUser
220
222
}
221
223
222
224
func (p *Pages) Login(w io.Writer, params LoginParams) error {
···
236
238
}
237
239
238
240
type TermsOfServiceParams struct {
239
-
LoggedInUser *oauth.User
241
+
LoggedInUser *oauth.MultiAccountUser
240
242
Content template.HTML
241
243
}
242
244
···
264
266
}
265
267
266
268
type PrivacyPolicyParams struct {
267
-
LoggedInUser *oauth.User
269
+
LoggedInUser *oauth.MultiAccountUser
268
270
Content template.HTML
269
271
}
270
272
···
292
294
}
293
295
294
296
type BrandParams struct {
295
-
LoggedInUser *oauth.User
297
+
LoggedInUser *oauth.MultiAccountUser
296
298
}
297
299
298
300
func (p *Pages) Brand(w io.Writer, params BrandParams) error {
···
300
302
}
301
303
302
304
type TimelineParams struct {
303
-
LoggedInUser *oauth.User
305
+
LoggedInUser *oauth.MultiAccountUser
304
306
Timeline []models.TimelineEvent
305
307
Repos []models.Repo
306
308
GfiLabel *models.LabelDefinition
···
311
313
}
312
314
313
315
type GoodFirstIssuesParams struct {
314
-
LoggedInUser *oauth.User
316
+
LoggedInUser *oauth.MultiAccountUser
315
317
Issues []models.Issue
316
318
RepoGroups []*models.RepoGroup
317
319
LabelDefs map[string]*models.LabelDefinition
···
324
326
}
325
327
326
328
type UserProfileSettingsParams struct {
327
-
LoggedInUser *oauth.User
329
+
LoggedInUser *oauth.MultiAccountUser
328
330
Tabs []map[string]any
329
331
Tab string
330
332
}
···
334
336
}
335
337
336
338
type NotificationsParams struct {
337
-
LoggedInUser *oauth.User
339
+
LoggedInUser *oauth.MultiAccountUser
338
340
Notifications []*models.NotificationWithEntity
339
341
UnreadCount int
340
342
Page pagination.Page
···
362
364
}
363
365
364
366
type UserKeysSettingsParams struct {
365
-
LoggedInUser *oauth.User
367
+
LoggedInUser *oauth.MultiAccountUser
366
368
PubKeys []models.PublicKey
367
369
Tabs []map[string]any
368
370
Tab string
···
373
375
}
374
376
375
377
type UserEmailsSettingsParams struct {
376
-
LoggedInUser *oauth.User
378
+
LoggedInUser *oauth.MultiAccountUser
377
379
Emails []models.Email
378
380
Tabs []map[string]any
379
381
Tab string
···
384
386
}
385
387
386
388
type UserNotificationSettingsParams struct {
387
-
LoggedInUser *oauth.User
389
+
LoggedInUser *oauth.MultiAccountUser
388
390
Preferences *models.NotificationPreferences
389
391
Tabs []map[string]any
390
392
Tab string
···
404
406
}
405
407
406
408
type KnotsParams struct {
407
-
LoggedInUser *oauth.User
409
+
LoggedInUser *oauth.MultiAccountUser
408
410
Registrations []models.Registration
409
411
Tabs []map[string]any
410
412
Tab string
···
415
417
}
416
418
417
419
type KnotParams struct {
418
-
LoggedInUser *oauth.User
420
+
LoggedInUser *oauth.MultiAccountUser
419
421
Registration *models.Registration
420
422
Members []string
421
423
Repos map[string][]models.Repo
···
437
439
}
438
440
439
441
type SpindlesParams struct {
440
-
LoggedInUser *oauth.User
442
+
LoggedInUser *oauth.MultiAccountUser
441
443
Spindles []models.Spindle
442
444
Tabs []map[string]any
443
445
Tab string
···
458
460
}
459
461
460
462
type SpindleDashboardParams struct {
461
-
LoggedInUser *oauth.User
463
+
LoggedInUser *oauth.MultiAccountUser
462
464
Spindle models.Spindle
463
465
Members []string
464
466
Repos map[string][]models.Repo
···
471
473
}
472
474
473
475
type NewRepoParams struct {
474
-
LoggedInUser *oauth.User
476
+
LoggedInUser *oauth.MultiAccountUser
475
477
Knots []string
476
478
}
477
479
···
480
482
}
481
483
482
484
type ForkRepoParams struct {
483
-
LoggedInUser *oauth.User
485
+
LoggedInUser *oauth.MultiAccountUser
484
486
Knots []string
485
487
RepoInfo repoinfo.RepoInfo
486
488
}
···
518
520
}
519
521
520
522
type ProfileOverviewParams struct {
521
-
LoggedInUser *oauth.User
523
+
LoggedInUser *oauth.MultiAccountUser
522
524
Repos []models.Repo
523
525
CollaboratingRepos []models.Repo
524
526
ProfileTimeline *models.ProfileTimeline
···
532
534
}
533
535
534
536
type ProfileReposParams struct {
535
-
LoggedInUser *oauth.User
537
+
LoggedInUser *oauth.MultiAccountUser
536
538
Repos []models.Repo
537
539
Card *ProfileCard
538
540
Active string
···
544
546
}
545
547
546
548
type ProfileStarredParams struct {
547
-
LoggedInUser *oauth.User
549
+
LoggedInUser *oauth.MultiAccountUser
548
550
Repos []models.Repo
549
551
Card *ProfileCard
550
552
Active string
···
556
558
}
557
559
558
560
type ProfileStringsParams struct {
559
-
LoggedInUser *oauth.User
561
+
LoggedInUser *oauth.MultiAccountUser
560
562
Strings []models.String
561
563
Card *ProfileCard
562
564
Active string
···
569
571
570
572
type FollowCard struct {
571
573
UserDid string
572
-
LoggedInUser *oauth.User
574
+
LoggedInUser *oauth.MultiAccountUser
573
575
FollowStatus models.FollowStatus
574
576
FollowersCount int64
575
577
FollowingCount int64
···
577
579
}
578
580
579
581
type ProfileFollowersParams struct {
580
-
LoggedInUser *oauth.User
582
+
LoggedInUser *oauth.MultiAccountUser
581
583
Followers []FollowCard
582
584
Card *ProfileCard
583
585
Active string
···
589
591
}
590
592
591
593
type ProfileFollowingParams struct {
592
-
LoggedInUser *oauth.User
594
+
LoggedInUser *oauth.MultiAccountUser
593
595
Following []FollowCard
594
596
Card *ProfileCard
595
597
Active string
···
610
612
}
611
613
612
614
type EditBioParams struct {
613
-
LoggedInUser *oauth.User
615
+
LoggedInUser *oauth.MultiAccountUser
614
616
Profile *models.Profile
615
617
}
616
618
···
619
621
}
620
622
621
623
type EditPinsParams struct {
622
-
LoggedInUser *oauth.User
624
+
LoggedInUser *oauth.MultiAccountUser
623
625
Profile *models.Profile
624
626
AllRepos []PinnedRepo
625
627
}
···
644
646
}
645
647
646
648
type RepoIndexParams struct {
647
-
LoggedInUser *oauth.User
649
+
LoggedInUser *oauth.MultiAccountUser
648
650
RepoInfo repoinfo.RepoInfo
649
651
Active string
650
652
TagMap map[string][]string
···
693
695
}
694
696
695
697
type RepoLogParams struct {
696
-
LoggedInUser *oauth.User
698
+
LoggedInUser *oauth.MultiAccountUser
697
699
RepoInfo repoinfo.RepoInfo
698
700
TagMap map[string][]string
699
701
Active string
···
710
712
}
711
713
712
714
type RepoCommitParams struct {
713
-
LoggedInUser *oauth.User
715
+
LoggedInUser *oauth.MultiAccountUser
714
716
RepoInfo repoinfo.RepoInfo
715
717
Active string
716
718
EmailToDid map[string]string
···
729
731
}
730
732
731
733
type RepoTreeParams struct {
732
-
LoggedInUser *oauth.User
734
+
LoggedInUser *oauth.MultiAccountUser
733
735
RepoInfo repoinfo.RepoInfo
734
736
Active string
735
737
BreadCrumbs [][]string
···
784
786
}
785
787
786
788
type RepoBranchesParams struct {
787
-
LoggedInUser *oauth.User
789
+
LoggedInUser *oauth.MultiAccountUser
788
790
RepoInfo repoinfo.RepoInfo
789
791
Active string
790
792
types.RepoBranchesResponse
···
796
798
}
797
799
798
800
type RepoTagsParams struct {
799
-
LoggedInUser *oauth.User
801
+
LoggedInUser *oauth.MultiAccountUser
800
802
RepoInfo repoinfo.RepoInfo
801
803
Active string
802
804
types.RepoTagsResponse
···
810
812
}
811
813
812
814
type RepoArtifactParams struct {
813
-
LoggedInUser *oauth.User
815
+
LoggedInUser *oauth.MultiAccountUser
814
816
RepoInfo repoinfo.RepoInfo
815
817
Artifact models.Artifact
816
818
}
···
820
822
}
821
823
822
824
type RepoBlobParams struct {
823
-
LoggedInUser *oauth.User
825
+
LoggedInUser *oauth.MultiAccountUser
824
826
RepoInfo repoinfo.RepoInfo
825
827
Active string
826
828
BreadCrumbs [][]string
···
844
846
}
845
847
846
848
type RepoSettingsParams struct {
847
-
LoggedInUser *oauth.User
849
+
LoggedInUser *oauth.MultiAccountUser
848
850
RepoInfo repoinfo.RepoInfo
849
851
Collaborators []Collaborator
850
852
Active string
···
863
865
}
864
866
865
867
type RepoGeneralSettingsParams struct {
866
-
LoggedInUser *oauth.User
868
+
LoggedInUser *oauth.MultiAccountUser
867
869
RepoInfo repoinfo.RepoInfo
868
870
Labels []models.LabelDefinition
869
871
DefaultLabels []models.LabelDefinition
···
881
883
}
882
884
883
885
type RepoAccessSettingsParams struct {
884
-
LoggedInUser *oauth.User
886
+
LoggedInUser *oauth.MultiAccountUser
885
887
RepoInfo repoinfo.RepoInfo
886
888
Active string
887
889
Tabs []map[string]any
···
895
897
}
896
898
897
899
type RepoPipelineSettingsParams struct {
898
-
LoggedInUser *oauth.User
900
+
LoggedInUser *oauth.MultiAccountUser
899
901
RepoInfo repoinfo.RepoInfo
900
902
Active string
901
903
Tabs []map[string]any
···
911
913
}
912
914
913
915
type RepoIssuesParams struct {
914
-
LoggedInUser *oauth.User
916
+
LoggedInUser *oauth.MultiAccountUser
915
917
RepoInfo repoinfo.RepoInfo
916
918
Active string
917
919
Issues []models.Issue
···
928
930
}
929
931
930
932
type RepoSingleIssueParams struct {
931
-
LoggedInUser *oauth.User
933
+
LoggedInUser *oauth.MultiAccountUser
932
934
RepoInfo repoinfo.RepoInfo
933
935
Active string
934
936
Issue *models.Issue
···
947
949
}
948
950
949
951
type EditIssueParams struct {
950
-
LoggedInUser *oauth.User
952
+
LoggedInUser *oauth.MultiAccountUser
951
953
RepoInfo repoinfo.RepoInfo
952
954
Issue *models.Issue
953
955
Action string
···
971
973
}
972
974
973
975
type RepoNewIssueParams struct {
974
-
LoggedInUser *oauth.User
976
+
LoggedInUser *oauth.MultiAccountUser
975
977
RepoInfo repoinfo.RepoInfo
976
978
Issue *models.Issue // existing issue if any -- passed when editing
977
979
Active string
···
985
987
}
986
988
987
989
type EditIssueCommentParams struct {
988
-
LoggedInUser *oauth.User
990
+
LoggedInUser *oauth.MultiAccountUser
989
991
RepoInfo repoinfo.RepoInfo
990
992
Issue *models.Issue
991
993
Comment *models.IssueComment
···
996
998
}
997
999
998
1000
type ReplyIssueCommentPlaceholderParams struct {
999
-
LoggedInUser *oauth.User
1001
+
LoggedInUser *oauth.MultiAccountUser
1000
1002
RepoInfo repoinfo.RepoInfo
1001
1003
Issue *models.Issue
1002
1004
Comment *models.IssueComment
···
1007
1009
}
1008
1010
1009
1011
type ReplyIssueCommentParams struct {
1010
-
LoggedInUser *oauth.User
1012
+
LoggedInUser *oauth.MultiAccountUser
1011
1013
RepoInfo repoinfo.RepoInfo
1012
1014
Issue *models.Issue
1013
1015
Comment *models.IssueComment
···
1018
1020
}
1019
1021
1020
1022
type IssueCommentBodyParams struct {
1021
-
LoggedInUser *oauth.User
1023
+
LoggedInUser *oauth.MultiAccountUser
1022
1024
RepoInfo repoinfo.RepoInfo
1023
1025
Issue *models.Issue
1024
1026
Comment *models.IssueComment
···
1029
1031
}
1030
1032
1031
1033
type RepoNewPullParams struct {
1032
-
LoggedInUser *oauth.User
1034
+
LoggedInUser *oauth.MultiAccountUser
1033
1035
RepoInfo repoinfo.RepoInfo
1034
1036
Branches []types.Branch
1035
1037
Strategy string
···
1046
1048
}
1047
1049
1048
1050
type RepoPullsParams struct {
1049
-
LoggedInUser *oauth.User
1051
+
LoggedInUser *oauth.MultiAccountUser
1050
1052
RepoInfo repoinfo.RepoInfo
1051
1053
Pulls []*models.Pull
1052
1054
Active string
···
1081
1083
}
1082
1084
1083
1085
type RepoSinglePullParams struct {
1084
-
LoggedInUser *oauth.User
1086
+
LoggedInUser *oauth.MultiAccountUser
1085
1087
RepoInfo repoinfo.RepoInfo
1086
1088
Active string
1087
1089
Pull *models.Pull
···
1106
1108
}
1107
1109
1108
1110
type RepoPullPatchParams struct {
1109
-
LoggedInUser *oauth.User
1111
+
LoggedInUser *oauth.MultiAccountUser
1110
1112
RepoInfo repoinfo.RepoInfo
1111
1113
Pull *models.Pull
1112
1114
Stack models.Stack
···
1123
1125
}
1124
1126
1125
1127
type RepoPullInterdiffParams struct {
1126
-
LoggedInUser *oauth.User
1128
+
LoggedInUser *oauth.MultiAccountUser
1127
1129
RepoInfo repoinfo.RepoInfo
1128
1130
Pull *models.Pull
1129
1131
Round int
···
1176
1178
}
1177
1179
1178
1180
type PullResubmitParams struct {
1179
-
LoggedInUser *oauth.User
1181
+
LoggedInUser *oauth.MultiAccountUser
1180
1182
RepoInfo repoinfo.RepoInfo
1181
1183
Pull *models.Pull
1182
1184
SubmissionId int
···
1187
1189
}
1188
1190
1189
1191
type PullActionsParams struct {
1190
-
LoggedInUser *oauth.User
1192
+
LoggedInUser *oauth.MultiAccountUser
1191
1193
RepoInfo repoinfo.RepoInfo
1192
1194
Pull *models.Pull
1193
1195
RoundNumber int
···
1202
1204
}
1203
1205
1204
1206
type PullNewCommentParams struct {
1205
-
LoggedInUser *oauth.User
1207
+
LoggedInUser *oauth.MultiAccountUser
1206
1208
RepoInfo repoinfo.RepoInfo
1207
1209
Pull *models.Pull
1208
1210
RoundNumber int
···
1213
1215
}
1214
1216
1215
1217
type RepoCompareParams struct {
1216
-
LoggedInUser *oauth.User
1218
+
LoggedInUser *oauth.MultiAccountUser
1217
1219
RepoInfo repoinfo.RepoInfo
1218
1220
Forks []models.Repo
1219
1221
Branches []types.Branch
···
1232
1234
}
1233
1235
1234
1236
type RepoCompareNewParams struct {
1235
-
LoggedInUser *oauth.User
1237
+
LoggedInUser *oauth.MultiAccountUser
1236
1238
RepoInfo repoinfo.RepoInfo
1237
1239
Forks []models.Repo
1238
1240
Branches []types.Branch
···
1249
1251
}
1250
1252
1251
1253
type RepoCompareAllowPullParams struct {
1252
-
LoggedInUser *oauth.User
1254
+
LoggedInUser *oauth.MultiAccountUser
1253
1255
RepoInfo repoinfo.RepoInfo
1254
1256
Base string
1255
1257
Head string
···
1269
1271
}
1270
1272
1271
1273
type LabelPanelParams struct {
1272
-
LoggedInUser *oauth.User
1274
+
LoggedInUser *oauth.MultiAccountUser
1273
1275
RepoInfo repoinfo.RepoInfo
1274
1276
Defs map[string]*models.LabelDefinition
1275
1277
Subject string
···
1281
1283
}
1282
1284
1283
1285
type EditLabelPanelParams struct {
1284
-
LoggedInUser *oauth.User
1286
+
LoggedInUser *oauth.MultiAccountUser
1285
1287
RepoInfo repoinfo.RepoInfo
1286
1288
Defs map[string]*models.LabelDefinition
1287
1289
Subject string
···
1293
1295
}
1294
1296
1295
1297
type PipelinesParams struct {
1296
-
LoggedInUser *oauth.User
1298
+
LoggedInUser *oauth.MultiAccountUser
1297
1299
RepoInfo repoinfo.RepoInfo
1298
1300
Pipelines []models.Pipeline
1299
1301
Active string
···
1336
1338
}
1337
1339
1338
1340
type WorkflowParams struct {
1339
-
LoggedInUser *oauth.User
1341
+
LoggedInUser *oauth.MultiAccountUser
1340
1342
RepoInfo repoinfo.RepoInfo
1341
1343
Pipeline models.Pipeline
1342
1344
Workflow string
···
1350
1352
}
1351
1353
1352
1354
type PutStringParams struct {
1353
-
LoggedInUser *oauth.User
1355
+
LoggedInUser *oauth.MultiAccountUser
1354
1356
Action string
1355
1357
1356
1358
// this is supplied in the case of editing an existing string
···
1362
1364
}
1363
1365
1364
1366
type StringsDashboardParams struct {
1365
-
LoggedInUser *oauth.User
1367
+
LoggedInUser *oauth.MultiAccountUser
1366
1368
Card ProfileCard
1367
1369
Strings []models.String
1368
1370
}
···
1372
1374
}
1373
1375
1374
1376
type StringTimelineParams struct {
1375
-
LoggedInUser *oauth.User
1377
+
LoggedInUser *oauth.MultiAccountUser
1376
1378
Strings []models.String
1377
1379
}
1378
1380
···
1381
1383
}
1382
1384
1383
1385
type SingleStringParams struct {
1384
-
LoggedInUser *oauth.User
1386
+
LoggedInUser *oauth.MultiAccountUser
1385
1387
ShowRendered bool
1386
1388
RenderToggle bool
1387
1389
RenderedContents template.HTML
+1
-1
appview/pages/templates/knots/index.html
+1
-1
appview/pages/templates/knots/index.html
···
105
105
{{ define "docsButton" }}
106
106
<a
107
107
class="btn flex items-center gap-2"
108
-
href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md">
108
+
href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide">
109
109
{{ i "book" "size-4" }}
110
110
docs
111
111
</a>
+49
-11
appview/pages/templates/layouts/fragments/topbar.html
+49
-11
appview/pages/templates/layouts/fragments/topbar.html
···
49
49
{{ define "profileDropdown" }}
50
50
<details class="relative inline-block text-left nav-dropdown">
51
51
<summary class="cursor-pointer list-none flex items-center gap-1">
52
-
{{ $user := .Did }}
52
+
{{ $user := .Active.Did }}
53
53
<img
54
54
src="{{ tinyAvatar $user }}"
55
55
alt=""
···
57
57
/>
58
58
<span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span>
59
59
</summary>
60
-
<div class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700">
61
-
<a href="/{{ $user }}">profile</a>
62
-
<a href="/{{ $user }}?tab=repos">repositories</a>
63
-
<a href="/{{ $user }}?tab=strings">strings</a>
64
-
<a href="/settings">settings</a>
65
-
<a href="#"
66
-
hx-post="/logout"
67
-
hx-swap="none"
68
-
class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
69
-
logout
60
+
<div class="absolute right-0 mt-4 p-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg z-50" style="width: 14rem;">
61
+
{{ $active := .Active.Did }}
62
+
63
+
<div class="pb-2 mb-2 border-b border-gray-200 dark:border-gray-700">
64
+
<div class="flex items-center gap-2">
65
+
<img src="{{ tinyAvatar $active }}" alt="" class="rounded-full h-8 w-8 flex-shrink-0 border border-gray-300 dark:border-gray-700" />
66
+
<div class="flex-1 overflow-hidden">
67
+
<p class="font-medium text-sm truncate">{{ $active | resolve }}</p>
68
+
<p class="text-xs text-green-600 dark:text-green-400">active</p>
69
+
</div>
70
+
</div>
71
+
</div>
72
+
73
+
{{ $others := .Accounts | otherAccounts $active }}
74
+
{{ if $others }}
75
+
<div class="pb-2 mb-2 border-b border-gray-200 dark:border-gray-700">
76
+
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">Switch Account</p>
77
+
{{ range $others }}
78
+
<button
79
+
type="button"
80
+
hx-post="/account/switch"
81
+
hx-vals='{"did": "{{ .Did }}"}'
82
+
hx-swap="none"
83
+
class="flex items-center gap-2 w-full py-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-left"
84
+
>
85
+
<img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full h-6 w-6 flex-shrink-0 border border-gray-300 dark:border-gray-700" />
86
+
<span class="text-sm truncate flex-1">{{ .Did | resolve }}</span>
87
+
</button>
88
+
{{ end }}
89
+
</div>
90
+
{{ end }}
91
+
92
+
<a href="/login?mode=add_account" class="flex items-center gap-2 py-1 text-sm">
93
+
{{ i "plus" "w-4 h-4 flex-shrink-0" }}
94
+
<span>Add another account</span>
70
95
</a>
96
+
97
+
<div class="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700 space-y-1">
98
+
<a href="/{{ $active }}" class="block py-1 text-sm">profile</a>
99
+
<a href="/{{ $active }}?tab=repos" class="block py-1 text-sm">repositories</a>
100
+
<a href="/{{ $active }}?tab=strings" class="block py-1 text-sm">strings</a>
101
+
<a href="/settings" class="block py-1 text-sm">settings</a>
102
+
<a href="#"
103
+
hx-post="/logout"
104
+
hx-swap="none"
105
+
class="block py-1 text-sm text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
106
+
logout
107
+
</a>
108
+
</div>
71
109
</div>
72
110
</details>
73
111
+1
-1
appview/pages/templates/repo/fragments/diff.html
+1
-1
appview/pages/templates/repo/fragments/diff.html
···
17
17
{{ else }}
18
18
{{ range $idx, $hunk := $diff }}
19
19
{{ with $hunk }}
20
-
<details open id="file-{{ .Name.New }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}">
20
+
<details open id="file-{{ .Id }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}">
21
21
<summary class="list-none cursor-pointer sticky top-0">
22
22
<div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between">
23
23
<div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto">
+35
-35
appview/pages/templates/repo/fragments/splitDiff.html
+35
-35
appview/pages/templates/repo/fragments/splitDiff.html
···
3
3
{{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}}
4
4
{{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}}
5
5
{{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}}
6
-
{{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}}
6
+
{{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}}
7
7
{{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}}
8
8
{{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}}
9
9
{{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
10
10
{{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
11
11
{{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}}
12
12
<div class="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700">
13
-
<pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</div>
13
+
<div class="overflow-x-auto col-span-1 font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</span>
14
14
{{- range .LeftLines -}}
15
15
{{- if .IsEmpty -}}
16
-
<div class="{{ $emptyStyle }} {{ $containerStyle }}">
17
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div>
18
-
<div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div>
19
-
<div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div>
20
-
</div>
16
+
<span class="{{ $emptyStyle }} {{ $containerStyle }}">
17
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span>
18
+
<span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span>
19
+
<span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span>
20
+
</span>
21
21
{{- else if eq .Op.String "-" -}}
22
-
<div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}">
23
-
<div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div>
24
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
25
-
<div class="px-2">{{ .Content }}</div>
26
-
</div>
22
+
<span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}">
23
+
<span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span>
24
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
25
+
<span class="px-2 whitespace-pre">{{ .Content }}</span>
26
+
</span>
27
27
{{- else if eq .Op.String " " -}}
28
-
<div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}">
29
-
<div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div>
30
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
31
-
<div class="px-2">{{ .Content }}</div>
32
-
</div>
28
+
<span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}">
29
+
<span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span>
30
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
31
+
<span class="px-2 whitespace-pre">{{ .Content }}</span>
32
+
</span>
33
33
{{- end -}}
34
34
{{- end -}}
35
-
{{- end -}}</div></div></pre>
35
+
{{- end -}}</div></div></div>
36
36
37
-
<pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</div>
37
+
<div class="overflow-x-auto col-span-1 font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</span>
38
38
{{- range .RightLines -}}
39
39
{{- if .IsEmpty -}}
40
-
<div class="{{ $emptyStyle }} {{ $containerStyle }}">
41
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div>
42
-
<div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div>
43
-
<div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div>
44
-
</div>
40
+
<span class="{{ $emptyStyle }} {{ $containerStyle }}">
41
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span>
42
+
<span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span>
43
+
<span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span>
44
+
</span>
45
45
{{- else if eq .Op.String "+" -}}
46
-
<div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}">
47
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div>
48
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
49
-
<div class="px-2" >{{ .Content }}</div>
50
-
</div>
46
+
<span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}">
47
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></span>
48
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
49
+
<span class="px-2 whitespace-pre">{{ .Content }}</span>
50
+
</span>
51
51
{{- else if eq .Op.String " " -}}
52
-
<div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}">
53
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div>
54
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
55
-
<div class="px-2">{{ .Content }}</div>
56
-
</div>
52
+
<span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}">
53
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a> </span>
54
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
55
+
<span class="px-2 whitespace-pre">{{ .Content }}</span>
56
+
</span>
57
57
{{- end -}}
58
58
{{- end -}}
59
-
{{- end -}}</div></div></pre>
59
+
{{- end -}}</div></div></div>
60
60
</div>
61
61
{{ end }}
+21
-22
appview/pages/templates/repo/fragments/unifiedDiff.html
+21
-22
appview/pages/templates/repo/fragments/unifiedDiff.html
···
1
1
{{ define "repo/fragments/unifiedDiff" }}
2
2
{{ $name := .Id }}
3
-
<pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</div>
3
+
<div class="overflow-x-auto font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</span>
4
4
{{- $oldStart := .OldPosition -}}
5
5
{{- $newStart := .NewPosition -}}
6
6
{{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}}
7
7
{{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}}
8
8
{{- $lineNrSepStyle1 := "" -}}
9
9
{{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}}
10
-
{{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}}
10
+
{{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}}
11
11
{{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}}
12
12
{{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
13
13
{{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
14
14
{{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}}
15
15
{{- range .Lines -}}
16
16
{{- if eq .Op.String "+" -}}
17
-
<div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}">
18
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div>
19
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div>
20
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
21
-
<div class="px-2">{{ .Line }}</div>
22
-
</div>
17
+
<span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}">
18
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></span>
19
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></span>
20
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
21
+
<span class="px-2 whitespace-pre">{{ .Line }}</span>
22
+
</span>
23
23
{{- $newStart = add64 $newStart 1 -}}
24
24
{{- end -}}
25
25
{{- if eq .Op.String "-" -}}
26
-
<div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}">
27
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div>
28
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div>
29
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
30
-
<div class="px-2">{{ .Line }}</div>
31
-
</div>
26
+
<span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}">
27
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></span>
28
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></span>
29
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
30
+
<span class="px-2 whitespace-pre">{{ .Line }}</span>
31
+
</span>
32
32
{{- $oldStart = add64 $oldStart 1 -}}
33
33
{{- end -}}
34
34
{{- if eq .Op.String " " -}}
35
-
<div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}">
36
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></div>
37
-
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></div>
38
-
<div class="{{ $opStyle }}">{{ .Op.String }}</div>
39
-
<div class="px-2">{{ .Line }}</div>
40
-
</div>
35
+
<span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}">
36
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></span>
37
+
<span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></span>
38
+
<span class="{{ $opStyle }}">{{ .Op.String }}</span>
39
+
<span class="px-2 whitespace-pre">{{ .Line }}</span>
40
+
</span>
41
41
{{- $newStart = add64 $newStart 1 -}}
42
42
{{- $oldStart = add64 $oldStart 1 -}}
43
43
{{- end -}}
44
44
{{- end -}}
45
-
{{- end -}}</div></div></pre>
45
+
{{- end -}}</div></div></div>
46
46
{{ end }}
47
-
+1
-1
appview/pages/templates/repo/pipelines/pipelines.html
+1
-1
appview/pages/templates/repo/pipelines/pipelines.html
···
23
23
</p>
24
24
<p>
25
25
<span class="{{ $bullet }}">2</span>Configure your CI/CD
26
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/pipeline.md" class="underline">pipeline</a>.
26
+
<a href="https://docs.tangled.org/spindles.html#pipelines" class="underline">pipeline</a>.
27
27
</p>
28
28
<p><span class="{{ $bullet }}">3</span>Trigger a workflow with a push or a pull-request!</p>
29
29
</div>
+1
-1
appview/pages/templates/repo/settings/pipelines.html
+1
-1
appview/pages/templates/repo/settings/pipelines.html
···
22
22
<p class="text-gray-500 dark:text-gray-400">
23
23
Choose a spindle to execute your workflows on. Only repository owners
24
24
can configure spindles. Spindles can be selfhosted,
25
-
<a class="text-gray-500 dark:text-gray-400 underline" href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
25
+
<a class="text-gray-500 dark:text-gray-400 underline" href="https://docs.tangled.org/spindles.html#self-hosting-guide">
26
26
click to learn more.
27
27
</a>
28
28
</p>
+1
-1
appview/pages/templates/spindles/index.html
+1
-1
appview/pages/templates/spindles/index.html
···
102
102
{{ define "docsButton" }}
103
103
<a
104
104
class="btn flex items-center gap-2"
105
-
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
105
+
href="https://docs.tangled.org/spindles.html#self-hosting-guide">
106
106
{{ i "book" "size-4" }}
107
107
docs
108
108
</a>
+53
appview/pages/templates/user/login.html
+53
appview/pages/templates/user/login.html
···
20
20
<h2 class="text-center text-xl italic dark:text-white">
21
21
tightly-knit social coding.
22
22
</h2>
23
+
24
+
{{ if .AddAccount }}
25
+
<div class="flex gap-2 my-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-300 dark:border-sky-800 rounded px-3 py-2 text-blue-600 dark:text-blue-300">
26
+
<span class="py-1">{{ i "user-plus" "w-4 h-4" }}</span>
27
+
<div>
28
+
<h5 class="font-medium">Add another account</h5>
29
+
<p class="text-sm">Sign in with a different account to add it to your account list.</p>
30
+
</div>
31
+
</div>
32
+
{{ end }}
33
+
34
+
{{ if and .LoggedInUser .LoggedInUser.Accounts }}
35
+
{{ $accounts := .LoggedInUser.Accounts }}
36
+
{{ if $accounts }}
37
+
<div class="my-4 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
38
+
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
39
+
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium">Saved accounts</span>
40
+
</div>
41
+
<div class="divide-y divide-gray-200 dark:divide-gray-700">
42
+
{{ range $accounts }}
43
+
<div class="flex items-center justify-between px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">
44
+
<button
45
+
type="button"
46
+
hx-post="/account/switch"
47
+
hx-vals='{"did": "{{ .Did }}"}'
48
+
hx-swap="none"
49
+
class="flex items-center gap-2 flex-1 text-left min-w-0"
50
+
>
51
+
<img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full h-8 w-8 flex-shrink-0 border border-gray-300 dark:border-gray-700" />
52
+
<div class="flex flex-col min-w-0">
53
+
<span class="text-sm font-medium dark:text-white truncate">{{ .Did | resolve | truncateAt30 }}</span>
54
+
<span class="text-xs text-gray-500 dark:text-gray-400">Click to switch</span>
55
+
</div>
56
+
</button>
57
+
<button
58
+
type="button"
59
+
hx-delete="/account/{{ .Did }}"
60
+
hx-swap="none"
61
+
class="p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 flex-shrink-0"
62
+
title="Remove account"
63
+
>
64
+
{{ i "x" "w-4 h-4" }}
65
+
</button>
66
+
</div>
67
+
{{ end }}
68
+
</div>
69
+
</div>
70
+
{{ end }}
71
+
{{ end }}
72
+
23
73
<form
24
74
class="mt-4"
25
75
hx-post="/login"
···
46
96
</span>
47
97
</div>
48
98
<input type="hidden" name="return_url" value="{{ .ReturnUrl }}">
99
+
<input type="hidden" name="add_account" value="{{ if .AddAccount }}true{{ end }}">
49
100
50
101
<button
51
102
class="btn w-full my-2 mt-6 text-base "
···
66
117
You have not authorized the app.
67
118
{{ else if eq .ErrorCode "session" }}
68
119
Server failed to create user session.
120
+
{{ else if eq .ErrorCode "max_accounts" }}
121
+
You have reached the maximum of 20 linked accounts. Please remove an account before adding a new one.
69
122
{{ else }}
70
123
Internal Server error.
71
124
{{ end }}
+2
-2
appview/pipelines/pipelines.go
+2
-2
appview/pipelines/pipelines.go
···
70
70
}
71
71
72
72
func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
73
-
user := p.oauth.GetUser(r)
73
+
user := p.oauth.GetMultiAccountUser(r)
74
74
l := p.logger.With("handler", "Index")
75
75
76
76
f, err := p.repoResolver.Resolve(r)
···
99
99
}
100
100
101
101
func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) {
102
-
user := p.oauth.GetUser(r)
102
+
user := p.oauth.GetMultiAccountUser(r)
103
103
l := p.logger.With("handler", "Workflow")
104
104
105
105
f, err := p.repoResolver.Resolve(r)
+103
-91
appview/pulls/pulls.go
+103
-91
appview/pulls/pulls.go
···
93
93
func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) {
94
94
switch r.Method {
95
95
case http.MethodGet:
96
-
user := s.oauth.GetUser(r)
96
+
user := s.oauth.GetMultiAccountUser(r)
97
97
f, err := s.repoResolver.Resolve(r)
98
98
if err != nil {
99
99
log.Println("failed to get repo and knot", err)
···
124
124
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
125
125
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
126
126
resubmitResult := pages.Unknown
127
-
if user.Did == pull.OwnerDid {
127
+
if user.Active.Did == pull.OwnerDid {
128
128
resubmitResult = s.resubmitCheck(r, f, pull, stack)
129
129
}
130
130
···
143
143
}
144
144
145
145
func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
146
-
user := s.oauth.GetUser(r)
146
+
user := s.oauth.GetMultiAccountUser(r)
147
147
f, err := s.repoResolver.Resolve(r)
148
148
if err != nil {
149
149
log.Println("failed to get repo and knot", err)
···
171
171
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
172
172
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
173
173
resubmitResult := pages.Unknown
174
-
if user != nil && user.Did == pull.OwnerDid {
174
+
if user != nil && user.Active != nil && user.Active.Did == pull.OwnerDid {
175
175
resubmitResult = s.resubmitCheck(r, f, pull, stack)
176
176
}
177
177
···
213
213
214
214
userReactions := map[models.ReactionKind]bool{}
215
215
if user != nil {
216
-
userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.AtUri())
216
+
userReactions = db.GetReactionStatusMap(s.db, user.Active.Did, pull.AtUri())
217
217
}
218
218
219
219
labelDefs, err := db.GetLabelDefinitions(
···
324
324
return nil
325
325
}
326
326
327
-
user := s.oauth.GetUser(r)
327
+
user := s.oauth.GetMultiAccountUser(r)
328
328
if user == nil {
329
329
return nil
330
330
}
···
347
347
}
348
348
349
349
// user can only delete branch if they are a collaborator in the repo that the branch belongs to
350
-
perms := s.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo())
350
+
perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo())
351
351
if !slices.Contains(perms, "repo:push") {
352
352
return nil
353
353
}
···
434
434
}
435
435
436
436
func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
437
-
user := s.oauth.GetUser(r)
437
+
user := s.oauth.GetMultiAccountUser(r)
438
438
439
439
var diffOpts types.DiffOpts
440
440
if d := r.URL.Query().Get("diff"); d == "split" {
···
475
475
}
476
476
477
477
func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
478
-
user := s.oauth.GetUser(r)
478
+
user := s.oauth.GetMultiAccountUser(r)
479
479
480
480
var diffOpts types.DiffOpts
481
481
if d := r.URL.Query().Get("diff"); d == "split" {
···
520
520
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
521
521
522
522
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
523
-
LoggedInUser: s.oauth.GetUser(r),
523
+
LoggedInUser: s.oauth.GetMultiAccountUser(r),
524
524
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
525
525
Pull: pull,
526
526
Round: roundIdInt,
···
552
552
func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
553
553
l := s.logger.With("handler", "RepoPulls")
554
554
555
-
user := s.oauth.GetUser(r)
555
+
user := s.oauth.GetMultiAccountUser(r)
556
556
params := r.URL.Query()
557
557
558
558
state := models.PullOpen
···
680
680
}
681
681
682
682
s.pages.RepoPulls(w, pages.RepoPullsParams{
683
-
LoggedInUser: s.oauth.GetUser(r),
683
+
LoggedInUser: s.oauth.GetMultiAccountUser(r),
684
684
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
685
685
Pulls: pulls,
686
686
LabelDefs: defs,
···
692
692
}
693
693
694
694
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
695
-
user := s.oauth.GetUser(r)
695
+
user := s.oauth.GetMultiAccountUser(r)
696
696
f, err := s.repoResolver.Resolve(r)
697
697
if err != nil {
698
698
log.Println("failed to get repo and knot", err)
···
751
751
}
752
752
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
753
753
Collection: tangled.RepoPullCommentNSID,
754
-
Repo: user.Did,
754
+
Repo: user.Active.Did,
755
755
Rkey: tid.TID(),
756
756
Record: &lexutil.LexiconTypeDecoder{
757
757
Val: &tangled.RepoPullComment{
···
768
768
}
769
769
770
770
comment := &models.PullComment{
771
-
OwnerDid: user.Did,
771
+
OwnerDid: user.Active.Did,
772
772
RepoAt: f.RepoAt().String(),
773
773
PullId: pull.PullId,
774
774
Body: body,
···
802
802
}
803
803
804
804
func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) {
805
-
user := s.oauth.GetUser(r)
805
+
user := s.oauth.GetMultiAccountUser(r)
806
806
f, err := s.repoResolver.Resolve(r)
807
807
if err != nil {
808
808
log.Println("failed to get repo and knot", err)
···
870
870
}
871
871
872
872
// Determine PR type based on input parameters
873
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
873
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
874
874
isPushAllowed := roles.IsPushAllowed()
875
875
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
876
876
isForkBased := fromFork != "" && sourceBranch != ""
···
970
970
w http.ResponseWriter,
971
971
r *http.Request,
972
972
repo *models.Repo,
973
-
user *oauth.User,
973
+
user *oauth.MultiAccountUser,
974
974
title,
975
975
body,
976
976
targetBranch,
···
1027
1027
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1028
1028
}
1029
1029
1030
-
func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) {
1030
+
func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.MultiAccountUser, title, body, targetBranch, patch string, isStacked bool) {
1031
1031
if err := s.validator.ValidatePatch(&patch); err != nil {
1032
1032
s.logger.Error("patch validation failed", "err", err)
1033
1033
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
···
1037
1037
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked)
1038
1038
}
1039
1039
1040
-
func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) {
1040
+
func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.MultiAccountUser, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) {
1041
1041
repoString := strings.SplitN(forkRepo, "/", 2)
1042
1042
forkOwnerDid := repoString[0]
1043
1043
repoName := repoString[1]
···
1146
1146
w http.ResponseWriter,
1147
1147
r *http.Request,
1148
1148
repo *models.Repo,
1149
-
user *oauth.User,
1149
+
user *oauth.MultiAccountUser,
1150
1150
title, body, targetBranch string,
1151
1151
patch string,
1152
1152
combined string,
···
1218
1218
Title: title,
1219
1219
Body: body,
1220
1220
TargetBranch: targetBranch,
1221
-
OwnerDid: user.Did,
1221
+
OwnerDid: user.Active.Did,
1222
1222
RepoAt: repo.RepoAt(),
1223
1223
Rkey: rkey,
1224
1224
Mentions: mentions,
···
1241
1241
return
1242
1242
}
1243
1243
1244
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
1245
+
if err != nil {
1246
+
log.Println("failed to upload patch", err)
1247
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1248
+
return
1249
+
}
1250
+
1244
1251
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1245
1252
Collection: tangled.RepoPullNSID,
1246
-
Repo: user.Did,
1253
+
Repo: user.Active.Did,
1247
1254
Rkey: rkey,
1248
1255
Record: &lexutil.LexiconTypeDecoder{
1249
1256
Val: &tangled.RepoPull{
···
1252
1259
Repo: string(repo.RepoAt()),
1253
1260
Branch: targetBranch,
1254
1261
},
1255
-
Patch: patch,
1262
+
PatchBlob: blob.Blob,
1256
1263
Source: recordPullSource,
1257
1264
CreatedAt: time.Now().Format(time.RFC3339),
1258
1265
},
···
1280
1287
w http.ResponseWriter,
1281
1288
r *http.Request,
1282
1289
repo *models.Repo,
1283
-
user *oauth.User,
1290
+
user *oauth.MultiAccountUser,
1284
1291
targetBranch string,
1285
1292
patch string,
1286
1293
sourceRev string,
···
1328
1335
// apply all record creations at once
1329
1336
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1330
1337
for _, p := range stack {
1338
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch()))
1339
+
if err != nil {
1340
+
log.Println("failed to upload patch blob", err)
1341
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1342
+
return
1343
+
}
1344
+
1331
1345
record := p.AsRecord()
1332
-
write := comatproto.RepoApplyWrites_Input_Writes_Elem{
1346
+
record.PatchBlob = blob.Blob
1347
+
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
1333
1348
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
1334
1349
Collection: tangled.RepoPullNSID,
1335
1350
Rkey: &p.Rkey,
···
1337
1352
Val: &record,
1338
1353
},
1339
1354
},
1340
-
}
1341
-
writes = append(writes, &write)
1355
+
})
1342
1356
}
1343
1357
_, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
1344
-
Repo: user.Did,
1358
+
Repo: user.Active.Did,
1345
1359
Writes: writes,
1346
1360
})
1347
1361
if err != nil {
···
1413
1427
}
1414
1428
1415
1429
func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1416
-
user := s.oauth.GetUser(r)
1430
+
user := s.oauth.GetMultiAccountUser(r)
1417
1431
1418
1432
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1419
1433
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
···
1421
1435
}
1422
1436
1423
1437
func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
1424
-
user := s.oauth.GetUser(r)
1438
+
user := s.oauth.GetMultiAccountUser(r)
1425
1439
f, err := s.repoResolver.Resolve(r)
1426
1440
if err != nil {
1427
1441
log.Println("failed to get repo and knot", err)
···
1476
1490
}
1477
1491
1478
1492
func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1479
-
user := s.oauth.GetUser(r)
1493
+
user := s.oauth.GetMultiAccountUser(r)
1480
1494
1481
-
forks, err := db.GetForksByDid(s.db, user.Did)
1495
+
forks, err := db.GetForksByDid(s.db, user.Active.Did)
1482
1496
if err != nil {
1483
1497
log.Println("failed to get forks", err)
1484
1498
return
···
1492
1506
}
1493
1507
1494
1508
func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1495
-
user := s.oauth.GetUser(r)
1509
+
user := s.oauth.GetMultiAccountUser(r)
1496
1510
1497
1511
f, err := s.repoResolver.Resolve(r)
1498
1512
if err != nil {
···
1585
1599
}
1586
1600
1587
1601
func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1588
-
user := s.oauth.GetUser(r)
1602
+
user := s.oauth.GetMultiAccountUser(r)
1589
1603
1590
1604
pull, ok := r.Context().Value("pull").(*models.Pull)
1591
1605
if !ok {
···
1616
1630
}
1617
1631
1618
1632
func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1619
-
user := s.oauth.GetUser(r)
1633
+
user := s.oauth.GetMultiAccountUser(r)
1620
1634
1621
1635
pull, ok := r.Context().Value("pull").(*models.Pull)
1622
1636
if !ok {
···
1631
1645
return
1632
1646
}
1633
1647
1634
-
if user.Did != pull.OwnerDid {
1648
+
if user.Active.Did != pull.OwnerDid {
1635
1649
log.Println("unauthorized user")
1636
1650
w.WriteHeader(http.StatusUnauthorized)
1637
1651
return
···
1643
1657
}
1644
1658
1645
1659
func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1646
-
user := s.oauth.GetUser(r)
1660
+
user := s.oauth.GetMultiAccountUser(r)
1647
1661
1648
1662
pull, ok := r.Context().Value("pull").(*models.Pull)
1649
1663
if !ok {
···
1658
1672
return
1659
1673
}
1660
1674
1661
-
if user.Did != pull.OwnerDid {
1675
+
if user.Active.Did != pull.OwnerDid {
1662
1676
log.Println("unauthorized user")
1663
1677
w.WriteHeader(http.StatusUnauthorized)
1664
1678
return
1665
1679
}
1666
1680
1667
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
1681
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
1668
1682
if !roles.IsPushAllowed() {
1669
1683
log.Println("unauthorized user")
1670
1684
w.WriteHeader(http.StatusUnauthorized)
···
1708
1722
}
1709
1723
1710
1724
func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) {
1711
-
user := s.oauth.GetUser(r)
1725
+
user := s.oauth.GetMultiAccountUser(r)
1712
1726
1713
1727
pull, ok := r.Context().Value("pull").(*models.Pull)
1714
1728
if !ok {
···
1723
1737
return
1724
1738
}
1725
1739
1726
-
if user.Did != pull.OwnerDid {
1740
+
if user.Active.Did != pull.OwnerDid {
1727
1741
log.Println("unauthorized user")
1728
1742
w.WriteHeader(http.StatusUnauthorized)
1729
1743
return
···
1808
1822
w http.ResponseWriter,
1809
1823
r *http.Request,
1810
1824
repo *models.Repo,
1811
-
user *oauth.User,
1825
+
user *oauth.MultiAccountUser,
1812
1826
pull *models.Pull,
1813
1827
patch string,
1814
1828
combined string,
···
1864
1878
return
1865
1879
}
1866
1880
1867
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1881
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Active.Did, pull.Rkey)
1868
1882
if err != nil {
1869
1883
// failed to get record
1870
1884
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
1871
1885
return
1872
1886
}
1873
1887
1874
-
var recordPullSource *tangled.RepoPull_Source
1875
-
if pull.IsBranchBased() {
1876
-
recordPullSource = &tangled.RepoPull_Source{
1877
-
Branch: pull.PullSource.Branch,
1878
-
Sha: sourceRev,
1879
-
}
1888
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
1889
+
if err != nil {
1890
+
log.Println("failed to upload patch blob", err)
1891
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
1892
+
return
1880
1893
}
1881
-
if pull.IsForkBased() {
1882
-
repoAt := pull.PullSource.RepoAt.String()
1883
-
recordPullSource = &tangled.RepoPull_Source{
1884
-
Branch: pull.PullSource.Branch,
1885
-
Repo: &repoAt,
1886
-
Sha: sourceRev,
1887
-
}
1888
-
}
1894
+
record := pull.AsRecord()
1895
+
record.PatchBlob = blob.Blob
1896
+
record.CreatedAt = time.Now().Format(time.RFC3339)
1889
1897
1890
1898
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1891
1899
Collection: tangled.RepoPullNSID,
1892
-
Repo: user.Did,
1900
+
Repo: user.Active.Did,
1893
1901
Rkey: pull.Rkey,
1894
1902
SwapRecord: ex.Cid,
1895
1903
Record: &lexutil.LexiconTypeDecoder{
1896
-
Val: &tangled.RepoPull{
1897
-
Title: pull.Title,
1898
-
Target: &tangled.RepoPull_Target{
1899
-
Repo: string(repo.RepoAt()),
1900
-
Branch: pull.TargetBranch,
1901
-
},
1902
-
Patch: patch, // new patch
1903
-
Source: recordPullSource,
1904
-
CreatedAt: time.Now().Format(time.RFC3339),
1905
-
},
1904
+
Val: &record,
1906
1905
},
1907
1906
})
1908
1907
if err != nil {
···
1925
1924
w http.ResponseWriter,
1926
1925
r *http.Request,
1927
1926
repo *models.Repo,
1928
-
user *oauth.User,
1927
+
user *oauth.MultiAccountUser,
1929
1928
pull *models.Pull,
1930
1929
patch string,
1931
1930
stackId string,
···
1987
1986
return
1988
1987
}
1989
1988
defer tx.Rollback()
1989
+
1990
+
client, err := s.oauth.AuthorizedClient(r)
1991
+
if err != nil {
1992
+
log.Println("failed to authorize client")
1993
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1994
+
return
1995
+
}
1990
1996
1991
1997
// pds updates to make
1992
1998
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
···
2021
2027
return
2022
2028
}
2023
2029
2030
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
2031
+
if err != nil {
2032
+
log.Println("failed to upload patch blob", err)
2033
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
2034
+
return
2035
+
}
2024
2036
record := p.AsRecord()
2037
+
record.PatchBlob = blob.Blob
2025
2038
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
2026
2039
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
2027
2040
Collection: tangled.RepoPullNSID,
···
2056
2069
return
2057
2070
}
2058
2071
2072
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
2073
+
if err != nil {
2074
+
log.Println("failed to upload patch blob", err)
2075
+
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
2076
+
return
2077
+
}
2059
2078
record := np.AsRecord()
2060
-
2079
+
record.PatchBlob = blob.Blob
2061
2080
writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
2062
2081
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
2063
2082
Collection: tangled.RepoPullNSID,
···
2094
2113
return
2095
2114
}
2096
2115
2097
-
client, err := s.oauth.AuthorizedClient(r)
2098
-
if err != nil {
2099
-
log.Println("failed to authorize client")
2100
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
2101
-
return
2102
-
}
2103
-
2104
2116
_, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
2105
-
Repo: user.Did,
2117
+
Repo: user.Active.Did,
2106
2118
Writes: writes,
2107
2119
})
2108
2120
if err != nil {
···
2116
2128
}
2117
2129
2118
2130
func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
2119
-
user := s.oauth.GetUser(r)
2131
+
user := s.oauth.GetMultiAccountUser(r)
2120
2132
f, err := s.repoResolver.Resolve(r)
2121
2133
if err != nil {
2122
2134
log.Println("failed to resolve repo:", err)
···
2227
2239
2228
2240
// notify about the pull merge
2229
2241
for _, p := range pullsToMerge {
2230
-
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2242
+
s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p)
2231
2243
}
2232
2244
2233
2245
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
···
2235
2247
}
2236
2248
2237
2249
func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
2238
-
user := s.oauth.GetUser(r)
2250
+
user := s.oauth.GetMultiAccountUser(r)
2239
2251
2240
2252
f, err := s.repoResolver.Resolve(r)
2241
2253
if err != nil {
···
2251
2263
}
2252
2264
2253
2265
// auth filter: only owner or collaborators can close
2254
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
2266
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
2255
2267
isOwner := roles.IsOwner()
2256
2268
isCollaborator := roles.IsCollaborator()
2257
-
isPullAuthor := user.Did == pull.OwnerDid
2269
+
isPullAuthor := user.Active.Did == pull.OwnerDid
2258
2270
isCloseAllowed := isOwner || isCollaborator || isPullAuthor
2259
2271
if !isCloseAllowed {
2260
2272
log.Println("failed to close pull")
···
2300
2312
}
2301
2313
2302
2314
for _, p := range pullsToClose {
2303
-
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2315
+
s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p)
2304
2316
}
2305
2317
2306
2318
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
···
2308
2320
}
2309
2321
2310
2322
func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
2311
-
user := s.oauth.GetUser(r)
2323
+
user := s.oauth.GetMultiAccountUser(r)
2312
2324
2313
2325
f, err := s.repoResolver.Resolve(r)
2314
2326
if err != nil {
···
2325
2337
}
2326
2338
2327
2339
// auth filter: only owner or collaborators can close
2328
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
2340
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
2329
2341
isOwner := roles.IsOwner()
2330
2342
isCollaborator := roles.IsCollaborator()
2331
-
isPullAuthor := user.Did == pull.OwnerDid
2343
+
isPullAuthor := user.Active.Did == pull.OwnerDid
2332
2344
isCloseAllowed := isOwner || isCollaborator || isPullAuthor
2333
2345
if !isCloseAllowed {
2334
2346
log.Println("failed to close pull")
···
2374
2386
}
2375
2387
2376
2388
for _, p := range pullsToReopen {
2377
-
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2389
+
s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p)
2378
2390
}
2379
2391
2380
2392
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2381
2393
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2382
2394
}
2383
2395
2384
-
func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
2396
+
func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.MultiAccountUser, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
2385
2397
formatPatches, err := patchutil.ExtractPatches(patch)
2386
2398
if err != nil {
2387
2399
return nil, fmt.Errorf("Failed to extract patches: %v", err)
···
2417
2429
Title: title,
2418
2430
Body: body,
2419
2431
TargetBranch: targetBranch,
2420
-
OwnerDid: user.Did,
2432
+
OwnerDid: user.Active.Did,
2421
2433
RepoAt: repo.RepoAt(),
2422
2434
Rkey: rkey,
2423
2435
Mentions: mentions,
+1
appview/repo/archive.go
+1
appview/repo/archive.go
···
18
18
l := rp.logger.With("handler", "DownloadArchive")
19
19
ref := chi.URLParam(r, "ref")
20
20
ref, _ = url.PathUnescape(ref)
21
+
ref = strings.TrimSuffix(ref, ".tar.gz")
21
22
f, err := rp.repoResolver.Resolve(r)
22
23
if err != nil {
23
24
l.Error("failed to get repo and knot", "err", err)
+6
-6
appview/repo/artifact.go
+6
-6
appview/repo/artifact.go
···
30
30
31
31
// TODO: proper statuses here on early exit
32
32
func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) {
33
-
user := rp.oauth.GetUser(r)
33
+
user := rp.oauth.GetMultiAccountUser(r)
34
34
tagParam := chi.URLParam(r, "tag")
35
35
f, err := rp.repoResolver.Resolve(r)
36
36
if err != nil {
···
75
75
76
76
putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
77
77
Collection: tangled.RepoArtifactNSID,
78
-
Repo: user.Did,
78
+
Repo: user.Active.Did,
79
79
Rkey: rkey,
80
80
Record: &lexutil.LexiconTypeDecoder{
81
81
Val: &tangled.RepoArtifact{
···
104
104
defer tx.Rollback()
105
105
106
106
artifact := models.Artifact{
107
-
Did: user.Did,
107
+
Did: user.Active.Did,
108
108
Rkey: rkey,
109
109
RepoAt: f.RepoAt(),
110
110
Tag: tag.Tag.Hash,
···
220
220
221
221
// TODO: proper statuses here on early exit
222
222
func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
223
-
user := rp.oauth.GetUser(r)
223
+
user := rp.oauth.GetMultiAccountUser(r)
224
224
tagParam := chi.URLParam(r, "tag")
225
225
filename := chi.URLParam(r, "file")
226
226
f, err := rp.repoResolver.Resolve(r)
···
251
251
252
252
artifact := artifacts[0]
253
253
254
-
if user.Did != artifact.Did {
254
+
if user.Active.Did != artifact.Did {
255
255
log.Println("user not authorized to delete artifact", err)
256
256
rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.")
257
257
return
···
259
259
260
260
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
261
261
Collection: tangled.RepoArtifactNSID,
262
-
Repo: user.Did,
262
+
Repo: user.Active.Did,
263
263
Rkey: artifact.Rkey,
264
264
})
265
265
if err != nil {
+1
-1
appview/repo/blob.go
+1
-1
appview/repo/blob.go
+1
-1
appview/repo/branches.go
+1
-1
appview/repo/branches.go
+2
-2
appview/repo/compare.go
+2
-2
appview/repo/compare.go
···
20
20
func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) {
21
21
l := rp.logger.With("handler", "RepoCompareNew")
22
22
23
-
user := rp.oauth.GetUser(r)
23
+
user := rp.oauth.GetMultiAccountUser(r)
24
24
f, err := rp.repoResolver.Resolve(r)
25
25
if err != nil {
26
26
l.Error("failed to get repo and knot", "err", err)
···
101
101
func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) {
102
102
l := rp.logger.With("handler", "RepoCompare")
103
103
104
-
user := rp.oauth.GetUser(r)
104
+
user := rp.oauth.GetMultiAccountUser(r)
105
105
f, err := rp.repoResolver.Resolve(r)
106
106
if err != nil {
107
107
l.Error("failed to get repo and knot", "err", err)
+1
-1
appview/repo/index.go
+1
-1
appview/repo/index.go
+2
-2
appview/repo/log.go
+2
-2
appview/repo/log.go
···
109
109
}
110
110
}
111
111
112
-
user := rp.oauth.GetUser(r)
112
+
user := rp.oauth.GetMultiAccountUser(r)
113
113
114
114
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true)
115
115
if err != nil {
···
197
197
l.Error("failed to GetVerifiedCommits", "err", err)
198
198
}
199
199
200
-
user := rp.oauth.GetUser(r)
200
+
user := rp.oauth.GetMultiAccountUser(r)
201
201
pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This})
202
202
if err != nil {
203
203
l.Error("failed to getPipelineStatuses", "err", err)
+34
-34
appview/repo/repo.go
+34
-34
appview/repo/repo.go
···
81
81
82
82
// modify the spindle configured for this repo
83
83
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
84
-
user := rp.oauth.GetUser(r)
84
+
user := rp.oauth.GetMultiAccountUser(r)
85
85
l := rp.logger.With("handler", "EditSpindle")
86
-
l = l.With("did", user.Did)
86
+
l = l.With("did", user.Active.Did)
87
87
88
88
errorId := "operation-error"
89
89
fail := func(msg string, err error) {
···
107
107
108
108
if !removingSpindle {
109
109
// ensure that this is a valid spindle for this user
110
-
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
110
+
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Active.Did)
111
111
if err != nil {
112
112
fail("Failed to find spindles. Try again later.", err)
113
113
return
···
168
168
}
169
169
170
170
func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) {
171
-
user := rp.oauth.GetUser(r)
171
+
user := rp.oauth.GetMultiAccountUser(r)
172
172
l := rp.logger.With("handler", "AddLabel")
173
-
l = l.With("did", user.Did)
173
+
l = l.With("did", user.Active.Did)
174
174
175
175
f, err := rp.repoResolver.Resolve(r)
176
176
if err != nil {
···
216
216
}
217
217
218
218
label := models.LabelDefinition{
219
-
Did: user.Did,
219
+
Did: user.Active.Did,
220
220
Rkey: tid.TID(),
221
221
Name: name,
222
222
ValueType: valueType,
···
327
327
}
328
328
329
329
func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) {
330
-
user := rp.oauth.GetUser(r)
330
+
user := rp.oauth.GetMultiAccountUser(r)
331
331
l := rp.logger.With("handler", "DeleteLabel")
332
-
l = l.With("did", user.Did)
332
+
l = l.With("did", user.Active.Did)
333
333
334
334
f, err := rp.repoResolver.Resolve(r)
335
335
if err != nil {
···
435
435
}
436
436
437
437
func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) {
438
-
user := rp.oauth.GetUser(r)
438
+
user := rp.oauth.GetMultiAccountUser(r)
439
439
l := rp.logger.With("handler", "SubscribeLabel")
440
-
l = l.With("did", user.Did)
440
+
l = l.With("did", user.Active.Did)
441
441
442
442
f, err := rp.repoResolver.Resolve(r)
443
443
if err != nil {
···
521
521
}
522
522
523
523
func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) {
524
-
user := rp.oauth.GetUser(r)
524
+
user := rp.oauth.GetMultiAccountUser(r)
525
525
l := rp.logger.With("handler", "UnsubscribeLabel")
526
-
l = l.With("did", user.Did)
526
+
l = l.With("did", user.Active.Did)
527
527
528
528
f, err := rp.repoResolver.Resolve(r)
529
529
if err != nil {
···
633
633
}
634
634
state := states[subject]
635
635
636
-
user := rp.oauth.GetUser(r)
636
+
user := rp.oauth.GetMultiAccountUser(r)
637
637
rp.pages.LabelPanel(w, pages.LabelPanelParams{
638
638
LoggedInUser: user,
639
639
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
···
681
681
}
682
682
state := states[subject]
683
683
684
-
user := rp.oauth.GetUser(r)
684
+
user := rp.oauth.GetMultiAccountUser(r)
685
685
rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
686
686
LoggedInUser: user,
687
687
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
···
692
692
}
693
693
694
694
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
695
-
user := rp.oauth.GetUser(r)
695
+
user := rp.oauth.GetMultiAccountUser(r)
696
696
l := rp.logger.With("handler", "AddCollaborator")
697
-
l = l.With("did", user.Did)
697
+
l = l.With("did", user.Active.Did)
698
698
699
699
f, err := rp.repoResolver.Resolve(r)
700
700
if err != nil {
···
723
723
return
724
724
}
725
725
726
-
if collaboratorIdent.DID.String() == user.Did {
726
+
if collaboratorIdent.DID.String() == user.Active.Did {
727
727
fail("You seem to be adding yourself as a collaborator.", nil)
728
728
return
729
729
}
···
738
738
}
739
739
740
740
// emit a record
741
-
currentUser := rp.oauth.GetUser(r)
741
+
currentUser := rp.oauth.GetMultiAccountUser(r)
742
742
rkey := tid.TID()
743
743
createdAt := time.Now()
744
744
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
745
745
Collection: tangled.RepoCollaboratorNSID,
746
-
Repo: currentUser.Did,
746
+
Repo: currentUser.Active.Did,
747
747
Rkey: rkey,
748
748
Record: &lexutil.LexiconTypeDecoder{
749
749
Val: &tangled.RepoCollaborator{
···
792
792
}
793
793
794
794
err = db.AddCollaborator(tx, models.Collaborator{
795
-
Did: syntax.DID(currentUser.Did),
795
+
Did: syntax.DID(currentUser.Active.Did),
796
796
Rkey: rkey,
797
797
SubjectDid: collaboratorIdent.DID,
798
798
RepoAt: f.RepoAt(),
···
822
822
}
823
823
824
824
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
825
-
user := rp.oauth.GetUser(r)
825
+
user := rp.oauth.GetMultiAccountUser(r)
826
826
l := rp.logger.With("handler", "DeleteRepo")
827
827
828
828
noticeId := "operation-error"
···
840
840
}
841
841
_, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{
842
842
Collection: tangled.RepoNSID,
843
-
Repo: user.Did,
843
+
Repo: user.Active.Did,
844
844
Rkey: f.Rkey,
845
845
})
846
846
if err != nil {
···
940
940
ref := chi.URLParam(r, "ref")
941
941
ref, _ = url.PathUnescape(ref)
942
942
943
-
user := rp.oauth.GetUser(r)
943
+
user := rp.oauth.GetMultiAccountUser(r)
944
944
f, err := rp.repoResolver.Resolve(r)
945
945
if err != nil {
946
946
l.Error("failed to resolve source repo", "err", err)
···
969
969
r.Context(),
970
970
client,
971
971
&tangled.RepoForkSync_Input{
972
-
Did: user.Did,
972
+
Did: user.Active.Did,
973
973
Name: f.Name,
974
974
Source: f.Source,
975
975
Branch: ref,
···
988
988
func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) {
989
989
l := rp.logger.With("handler", "ForkRepo")
990
990
991
-
user := rp.oauth.GetUser(r)
991
+
user := rp.oauth.GetMultiAccountUser(r)
992
992
f, err := rp.repoResolver.Resolve(r)
993
993
if err != nil {
994
994
l.Error("failed to resolve source repo", "err", err)
···
997
997
998
998
switch r.Method {
999
999
case http.MethodGet:
1000
-
user := rp.oauth.GetUser(r)
1001
-
knots, err := rp.enforcer.GetKnotsForUser(user.Did)
1000
+
user := rp.oauth.GetMultiAccountUser(r)
1001
+
knots, err := rp.enforcer.GetKnotsForUser(user.Active.Did)
1002
1002
if err != nil {
1003
1003
rp.pages.Notice(w, "repo", "Invalid user account.")
1004
1004
return
···
1020
1020
}
1021
1021
l = l.With("targetKnot", targetKnot)
1022
1022
1023
-
ok, err := rp.enforcer.E.Enforce(user.Did, targetKnot, targetKnot, "repo:create")
1023
+
ok, err := rp.enforcer.E.Enforce(user.Active.Did, targetKnot, targetKnot, "repo:create")
1024
1024
if err != nil || !ok {
1025
1025
rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
1026
1026
return
···
1037
1037
// in the user's account.
1038
1038
existingRepo, err := db.GetRepo(
1039
1039
rp.db,
1040
-
orm.FilterEq("did", user.Did),
1040
+
orm.FilterEq("did", user.Active.Did),
1041
1041
orm.FilterEq("name", forkName),
1042
1042
)
1043
1043
if err != nil {
···
1066
1066
// create an atproto record for this fork
1067
1067
rkey := tid.TID()
1068
1068
repo := &models.Repo{
1069
-
Did: user.Did,
1069
+
Did: user.Active.Did,
1070
1070
Name: forkName,
1071
1071
Knot: targetKnot,
1072
1072
Rkey: rkey,
···
1086
1086
1087
1087
atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{
1088
1088
Collection: tangled.RepoNSID,
1089
-
Repo: user.Did,
1089
+
Repo: user.Active.Did,
1090
1090
Rkey: rkey,
1091
1091
Record: &lexutil.LexiconTypeDecoder{
1092
1092
Val: &record,
···
1165
1165
}
1166
1166
1167
1167
// acls
1168
-
p, _ := securejoin.SecureJoin(user.Did, forkName)
1169
-
err = rp.enforcer.AddRepo(user.Did, targetKnot, p)
1168
+
p, _ := securejoin.SecureJoin(user.Active.Did, forkName)
1169
+
err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p)
1170
1170
if err != nil {
1171
1171
l.Error("failed to add ACLs", "err", err)
1172
1172
rp.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
1191
1191
aturi = ""
1192
1192
1193
1193
rp.notifier.NewRepo(r.Context(), repo)
1194
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, forkName))
1194
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName))
1195
1195
}
1196
1196
}
1197
1197
+5
-5
appview/repo/settings.go
+5
-5
appview/repo/settings.go
···
79
79
}
80
80
81
81
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
82
-
user := rp.oauth.GetUser(r)
82
+
user := rp.oauth.GetMultiAccountUser(r)
83
83
l := rp.logger.With("handler", "Secrets")
84
-
l = l.With("did", user.Did)
84
+
l = l.With("did", user.Active.Did)
85
85
86
86
f, err := rp.repoResolver.Resolve(r)
87
87
if err != nil {
···
185
185
l := rp.logger.With("handler", "generalSettings")
186
186
187
187
f, err := rp.repoResolver.Resolve(r)
188
-
user := rp.oauth.GetUser(r)
188
+
user := rp.oauth.GetMultiAccountUser(r)
189
189
190
190
scheme := "http"
191
191
if !rp.config.Core.Dev {
···
271
271
l := rp.logger.With("handler", "accessSettings")
272
272
273
273
f, err := rp.repoResolver.Resolve(r)
274
-
user := rp.oauth.GetUser(r)
274
+
user := rp.oauth.GetMultiAccountUser(r)
275
275
276
276
collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) {
277
277
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot)
···
318
318
l := rp.logger.With("handler", "pipelineSettings")
319
319
320
320
f, err := rp.repoResolver.Resolve(r)
321
-
user := rp.oauth.GetUser(r)
321
+
user := rp.oauth.GetMultiAccountUser(r)
322
322
323
323
// all spindles that the repo owner is a member of
324
324
spindles, err := rp.enforcer.GetSpindlesForUser(f.Did)
+1
-1
appview/repo/tree.go
+1
-1
appview/repo/tree.go
···
88
88
http.Redirect(w, r, redirectTo, http.StatusFound)
89
89
return
90
90
}
91
-
user := rp.oauth.GetUser(r)
91
+
user := rp.oauth.GetMultiAccountUser(r)
92
92
var breadcrumbs [][]string
93
93
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))})
94
94
if treePath != "" {
+30
-5
appview/reporesolver/resolver.go
+30
-5
appview/reporesolver/resolver.go
···
55
55
// 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo`
56
56
// 3. [x] remove `ResolvedRepo`
57
57
// 4. [ ] replace reporesolver to reposervice
58
-
func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.User) repoinfo.RepoInfo {
58
+
func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.MultiAccountUser) repoinfo.RepoInfo {
59
59
ownerId, ook := r.Context().Value("resolvedId").(identity.Identity)
60
60
repo, rok := r.Context().Value("repo").(*models.Repo)
61
61
if !ook || !rok {
···
63
63
}
64
64
65
65
// get dir/ref
66
-
currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath()))
66
+
currentDir := extractCurrentDir(r.URL.EscapedPath())
67
67
ref := chi.URLParam(r, "ref")
68
68
69
69
repoAt := repo.RepoAt()
70
70
isStarred := false
71
71
roles := repoinfo.RolesInRepo{}
72
-
if user != nil {
73
-
isStarred = db.GetStarStatus(rr.execer, user.Did, repoAt)
74
-
roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo())
72
+
if user != nil && user.Active != nil {
73
+
isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt)
74
+
roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo())
75
75
}
76
76
77
77
stats := repo.RepoStats
···
130
130
}
131
131
132
132
return repoInfo
133
+
}
134
+
135
+
// extractCurrentDir gets the current directory for markdown link resolution.
136
+
// for blob paths, returns the parent dir. for tree paths, returns the path itself.
137
+
//
138
+
// /@user/repo/blob/main/docs/README.md => docs
139
+
// /@user/repo/tree/main/docs => docs
140
+
func extractCurrentDir(fullPath string) string {
141
+
fullPath = strings.TrimPrefix(fullPath, "/")
142
+
143
+
blobPattern := regexp.MustCompile(`blob/[^/]+/(.*)$`)
144
+
if matches := blobPattern.FindStringSubmatch(fullPath); len(matches) > 1 {
145
+
return path.Dir(matches[1])
146
+
}
147
+
148
+
treePattern := regexp.MustCompile(`tree/[^/]+/(.*)$`)
149
+
if matches := treePattern.FindStringSubmatch(fullPath); len(matches) > 1 {
150
+
dir := strings.TrimSuffix(matches[1], "/")
151
+
if dir == "" {
152
+
return "."
153
+
}
154
+
return dir
155
+
}
156
+
157
+
return "."
133
158
}
134
159
135
160
// extractPathAfterRef gets the actual repository path
+22
appview/reporesolver/resolver_test.go
+22
appview/reporesolver/resolver_test.go
···
1
+
package reporesolver
2
+
3
+
import "testing"
4
+
5
+
func TestExtractCurrentDir(t *testing.T) {
6
+
tests := []struct {
7
+
path string
8
+
want string
9
+
}{
10
+
{"/@user/repo/blob/main/docs/README.md", "docs"},
11
+
{"/@user/repo/blob/main/README.md", "."},
12
+
{"/@user/repo/tree/main/docs", "docs"},
13
+
{"/@user/repo/tree/main/docs/", "docs"},
14
+
{"/@user/repo/tree/main", "."},
15
+
}
16
+
17
+
for _, tt := range tests {
18
+
if got := extractCurrentDir(tt.path); got != tt.want {
19
+
t.Errorf("extractCurrentDir(%q) = %q, want %q", tt.path, got, tt.want)
20
+
}
21
+
}
22
+
}
+6
-6
appview/settings/settings.go
+6
-6
appview/settings/settings.go
···
81
81
}
82
82
83
83
func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) {
84
-
user := s.OAuth.GetUser(r)
84
+
user := s.OAuth.GetMultiAccountUser(r)
85
85
86
86
s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{
87
87
LoggedInUser: user,
···
91
91
}
92
92
93
93
func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) {
94
-
user := s.OAuth.GetUser(r)
94
+
user := s.OAuth.GetMultiAccountUser(r)
95
95
did := s.OAuth.GetDid(r)
96
96
97
97
prefs, err := db.GetNotificationPreference(s.Db, did)
···
137
137
}
138
138
139
139
func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) {
140
-
user := s.OAuth.GetUser(r)
141
-
pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Did)
140
+
user := s.OAuth.GetMultiAccountUser(r)
141
+
pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Active.Did)
142
142
if err != nil {
143
143
log.Println(err)
144
144
}
···
152
152
}
153
153
154
154
func (s *Settings) emailsSettings(w http.ResponseWriter, r *http.Request) {
155
-
user := s.OAuth.GetUser(r)
156
-
emails, err := db.GetAllEmails(s.Db, user.Did)
155
+
user := s.OAuth.GetMultiAccountUser(r)
156
+
emails, err := db.GetAllEmails(s.Db, user.Active.Did)
157
157
if err != nil {
158
158
log.Println(err)
159
159
}
+41
-46
appview/spindles/spindles.go
+41
-46
appview/spindles/spindles.go
···
69
69
}
70
70
71
71
func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) {
72
-
user := s.OAuth.GetUser(r)
72
+
user := s.OAuth.GetMultiAccountUser(r)
73
73
all, err := db.GetSpindles(
74
74
s.Db,
75
-
orm.FilterEq("owner", user.Did),
75
+
orm.FilterEq("owner", user.Active.Did),
76
76
)
77
77
if err != nil {
78
78
s.Logger.Error("failed to fetch spindles", "err", err)
···
91
91
func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) {
92
92
l := s.Logger.With("handler", "dashboard")
93
93
94
-
user := s.OAuth.GetUser(r)
95
-
l = l.With("user", user.Did)
94
+
user := s.OAuth.GetMultiAccountUser(r)
95
+
l = l.With("user", user.Active.Did)
96
96
97
97
instance := chi.URLParam(r, "instance")
98
98
if instance == "" {
···
103
103
spindles, err := db.GetSpindles(
104
104
s.Db,
105
105
orm.FilterEq("instance", instance),
106
-
orm.FilterEq("owner", user.Did),
106
+
orm.FilterEq("owner", user.Active.Did),
107
107
orm.FilterIsNot("verified", "null"),
108
108
)
109
109
if err != nil || len(spindles) != 1 {
···
155
155
//
156
156
// if the spindle is not up yet, the user is free to retry verification at a later point
157
157
func (s *Spindles) register(w http.ResponseWriter, r *http.Request) {
158
-
user := s.OAuth.GetUser(r)
158
+
user := s.OAuth.GetMultiAccountUser(r)
159
159
l := s.Logger.With("handler", "register")
160
160
161
161
noticeId := "register-error"
···
176
176
return
177
177
}
178
178
l = l.With("instance", instance)
179
-
l = l.With("user", user.Did)
179
+
l = l.With("user", user.Active.Did)
180
180
181
181
tx, err := s.Db.Begin()
182
182
if err != nil {
···
190
190
}()
191
191
192
192
err = db.AddSpindle(tx, models.Spindle{
193
-
Owner: syntax.DID(user.Did),
193
+
Owner: syntax.DID(user.Active.Did),
194
194
Instance: instance,
195
195
})
196
196
if err != nil {
···
214
214
return
215
215
}
216
216
217
-
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Did, instance)
217
+
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Active.Did, instance)
218
218
var exCid *string
219
219
if ex != nil {
220
220
exCid = ex.Cid
···
223
223
// re-announce by registering under same rkey
224
224
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
225
225
Collection: tangled.SpindleNSID,
226
-
Repo: user.Did,
226
+
Repo: user.Active.Did,
227
227
Rkey: instance,
228
228
Record: &lexutil.LexiconTypeDecoder{
229
229
Val: &tangled.Spindle{
···
254
254
}
255
255
256
256
// begin verification
257
-
err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
257
+
err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev)
258
258
if err != nil {
259
259
l.Error("verification failed", "err", err)
260
260
s.Pages.HxRefresh(w)
261
261
return
262
262
}
263
263
264
-
_, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did)
264
+
_, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did)
265
265
if err != nil {
266
266
l.Error("failed to mark verified", "err", err)
267
267
s.Pages.HxRefresh(w)
···
273
273
}
274
274
275
275
func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) {
276
-
user := s.OAuth.GetUser(r)
276
+
user := s.OAuth.GetMultiAccountUser(r)
277
277
l := s.Logger.With("handler", "delete")
278
278
279
279
noticeId := "operation-error"
···
291
291
292
292
spindles, err := db.GetSpindles(
293
293
s.Db,
294
-
orm.FilterEq("owner", user.Did),
294
+
orm.FilterEq("owner", user.Active.Did),
295
295
orm.FilterEq("instance", instance),
296
296
)
297
297
if err != nil || len(spindles) != 1 {
···
300
300
return
301
301
}
302
302
303
-
if string(spindles[0].Owner) != user.Did {
304
-
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
303
+
if string(spindles[0].Owner) != user.Active.Did {
304
+
l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner)
305
305
s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.")
306
306
return
307
307
}
···
320
320
// remove spindle members first
321
321
err = db.RemoveSpindleMember(
322
322
tx,
323
-
orm.FilterEq("did", user.Did),
323
+
orm.FilterEq("did", user.Active.Did),
324
324
orm.FilterEq("instance", instance),
325
325
)
326
326
if err != nil {
···
331
331
332
332
err = db.DeleteSpindle(
333
333
tx,
334
-
orm.FilterEq("owner", user.Did),
334
+
orm.FilterEq("owner", user.Active.Did),
335
335
orm.FilterEq("instance", instance),
336
336
)
337
337
if err != nil {
···
359
359
360
360
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
361
361
Collection: tangled.SpindleNSID,
362
-
Repo: user.Did,
362
+
Repo: user.Active.Did,
363
363
Rkey: instance,
364
364
})
365
365
if err != nil {
···
391
391
}
392
392
393
393
func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) {
394
-
user := s.OAuth.GetUser(r)
394
+
user := s.OAuth.GetMultiAccountUser(r)
395
395
l := s.Logger.With("handler", "retry")
396
396
397
397
noticeId := "operation-error"
···
407
407
return
408
408
}
409
409
l = l.With("instance", instance)
410
-
l = l.With("user", user.Did)
410
+
l = l.With("user", user.Active.Did)
411
411
412
412
spindles, err := db.GetSpindles(
413
413
s.Db,
414
-
orm.FilterEq("owner", user.Did),
414
+
orm.FilterEq("owner", user.Active.Did),
415
415
orm.FilterEq("instance", instance),
416
416
)
417
417
if err != nil || len(spindles) != 1 {
···
420
420
return
421
421
}
422
422
423
-
if string(spindles[0].Owner) != user.Did {
424
-
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
423
+
if string(spindles[0].Owner) != user.Active.Did {
424
+
l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner)
425
425
s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.")
426
426
return
427
427
}
428
428
429
429
// begin verification
430
-
err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
430
+
err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev)
431
431
if err != nil {
432
432
l.Error("verification failed", "err", err)
433
433
···
445
445
return
446
446
}
447
447
448
-
rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did)
448
+
rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did)
449
449
if err != nil {
450
450
l.Error("failed to mark verified", "err", err)
451
451
s.Pages.Notice(w, noticeId, err.Error())
···
473
473
}
474
474
475
475
func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) {
476
-
user := s.OAuth.GetUser(r)
476
+
user := s.OAuth.GetMultiAccountUser(r)
477
477
l := s.Logger.With("handler", "addMember")
478
478
479
479
instance := chi.URLParam(r, "instance")
···
483
483
return
484
484
}
485
485
l = l.With("instance", instance)
486
-
l = l.With("user", user.Did)
486
+
l = l.With("user", user.Active.Did)
487
487
488
488
spindles, err := db.GetSpindles(
489
489
s.Db,
490
-
orm.FilterEq("owner", user.Did),
490
+
orm.FilterEq("owner", user.Active.Did),
491
491
orm.FilterEq("instance", instance),
492
492
)
493
493
if err != nil || len(spindles) != 1 {
···
502
502
s.Pages.Notice(w, noticeId, defaultErr)
503
503
}
504
504
505
-
if string(spindles[0].Owner) != user.Did {
506
-
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
505
+
if string(spindles[0].Owner) != user.Active.Did {
506
+
l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner)
507
507
s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.")
508
508
return
509
509
}
···
552
552
553
553
// add member to db
554
554
if err = db.AddSpindleMember(tx, models.SpindleMember{
555
-
Did: syntax.DID(user.Did),
555
+
Did: syntax.DID(user.Active.Did),
556
556
Rkey: rkey,
557
557
Instance: instance,
558
558
Subject: memberId.DID,
···
570
570
571
571
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
572
572
Collection: tangled.SpindleMemberNSID,
573
-
Repo: user.Did,
573
+
Repo: user.Active.Did,
574
574
Rkey: rkey,
575
575
Record: &lexutil.LexiconTypeDecoder{
576
576
Val: &tangled.SpindleMember{
···
603
603
}
604
604
605
605
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
606
-
user := s.OAuth.GetUser(r)
606
+
user := s.OAuth.GetMultiAccountUser(r)
607
607
l := s.Logger.With("handler", "removeMember")
608
608
609
609
noticeId := "operation-error"
···
619
619
return
620
620
}
621
621
l = l.With("instance", instance)
622
-
l = l.With("user", user.Did)
622
+
l = l.With("user", user.Active.Did)
623
623
624
624
spindles, err := db.GetSpindles(
625
625
s.Db,
626
-
orm.FilterEq("owner", user.Did),
626
+
orm.FilterEq("owner", user.Active.Did),
627
627
orm.FilterEq("instance", instance),
628
628
)
629
629
if err != nil || len(spindles) != 1 {
···
632
632
return
633
633
}
634
634
635
-
if string(spindles[0].Owner) != user.Did {
636
-
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
635
+
if string(spindles[0].Owner) != user.Active.Did {
636
+
l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner)
637
637
s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.")
638
638
return
639
639
}
···
653
653
s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
654
654
return
655
655
}
656
-
if memberId.Handle.IsInvalidHandle() {
657
-
l.Error("failed to resolve member identity to handle")
658
-
s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
659
-
return
660
-
}
661
656
662
657
tx, err := s.Db.Begin()
663
658
if err != nil {
···
673
668
// get the record from the DB first:
674
669
members, err := db.GetSpindleMembers(
675
670
s.Db,
676
-
orm.FilterEq("did", user.Did),
671
+
orm.FilterEq("did", user.Active.Did),
677
672
orm.FilterEq("instance", instance),
678
673
orm.FilterEq("subject", memberId.DID),
679
674
)
···
686
681
// remove from db
687
682
if err = db.RemoveSpindleMember(
688
683
tx,
689
-
orm.FilterEq("did", user.Did),
684
+
orm.FilterEq("did", user.Active.Did),
690
685
orm.FilterEq("instance", instance),
691
686
orm.FilterEq("subject", memberId.DID),
692
687
); err != nil {
···
712
707
// remove from pds
713
708
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
714
709
Collection: tangled.SpindleMemberNSID,
715
-
Repo: user.Did,
710
+
Repo: user.Active.Did,
716
711
Rkey: members[0].Rkey,
717
712
})
718
713
if err != nil {
+83
appview/state/accounts.go
+83
appview/state/accounts.go
···
1
+
package state
2
+
3
+
import (
4
+
"net/http"
5
+
6
+
"github.com/go-chi/chi/v5"
7
+
)
8
+
9
+
func (s *State) SwitchAccount(w http.ResponseWriter, r *http.Request) {
10
+
l := s.logger.With("handler", "SwitchAccount")
11
+
12
+
if err := r.ParseForm(); err != nil {
13
+
l.Error("failed to parse form", "err", err)
14
+
http.Error(w, "invalid request", http.StatusBadRequest)
15
+
return
16
+
}
17
+
18
+
did := r.FormValue("did")
19
+
if did == "" {
20
+
http.Error(w, "missing did", http.StatusBadRequest)
21
+
return
22
+
}
23
+
24
+
if err := s.oauth.SwitchAccount(w, r, did); err != nil {
25
+
l.Error("failed to switch account", "err", err)
26
+
s.pages.HxRedirect(w, "/login?error=session")
27
+
return
28
+
}
29
+
30
+
l.Info("switched account", "did", did)
31
+
s.pages.HxRedirect(w, "/")
32
+
}
33
+
34
+
func (s *State) RemoveAccount(w http.ResponseWriter, r *http.Request) {
35
+
l := s.logger.With("handler", "RemoveAccount")
36
+
37
+
did := chi.URLParam(r, "did")
38
+
if did == "" {
39
+
http.Error(w, "missing did", http.StatusBadRequest)
40
+
return
41
+
}
42
+
43
+
currentUser := s.oauth.GetMultiAccountUser(r)
44
+
isCurrentAccount := currentUser != nil && currentUser.Active.Did == did
45
+
46
+
var remainingAccounts []string
47
+
if currentUser != nil {
48
+
for _, acc := range currentUser.Accounts {
49
+
if acc.Did != did {
50
+
remainingAccounts = append(remainingAccounts, acc.Did)
51
+
}
52
+
}
53
+
}
54
+
55
+
if err := s.oauth.RemoveAccount(w, r, did); err != nil {
56
+
l.Error("failed to remove account", "err", err)
57
+
http.Error(w, "failed to remove account", http.StatusInternalServerError)
58
+
return
59
+
}
60
+
61
+
l.Info("removed account", "did", did)
62
+
63
+
if isCurrentAccount {
64
+
if len(remainingAccounts) > 0 {
65
+
nextDid := remainingAccounts[0]
66
+
if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil {
67
+
l.Error("failed to switch to next account", "err", err)
68
+
s.pages.HxRedirect(w, "/login")
69
+
return
70
+
}
71
+
s.pages.HxRefresh(w)
72
+
return
73
+
}
74
+
75
+
if err := s.oauth.DeleteSession(w, r); err != nil {
76
+
l.Error("failed to delete session", "err", err)
77
+
}
78
+
s.pages.HxRedirect(w, "/login")
79
+
return
80
+
}
81
+
82
+
s.pages.HxRefresh(w)
83
+
}
+7
-7
appview/state/follow.go
+7
-7
appview/state/follow.go
···
15
15
)
16
16
17
17
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
18
-
currentUser := s.oauth.GetUser(r)
18
+
currentUser := s.oauth.GetMultiAccountUser(r)
19
19
20
20
subject := r.URL.Query().Get("subject")
21
21
if subject == "" {
···
29
29
return
30
30
}
31
31
32
-
if currentUser.Did == subjectIdent.DID.String() {
32
+
if currentUser.Active.Did == subjectIdent.DID.String() {
33
33
log.Println("cant follow or unfollow yourself")
34
34
return
35
35
}
···
46
46
rkey := tid.TID()
47
47
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
48
48
Collection: tangled.GraphFollowNSID,
49
-
Repo: currentUser.Did,
49
+
Repo: currentUser.Active.Did,
50
50
Rkey: rkey,
51
51
Record: &lexutil.LexiconTypeDecoder{
52
52
Val: &tangled.GraphFollow{
···
62
62
log.Println("created atproto record: ", resp.Uri)
63
63
64
64
follow := &models.Follow{
65
-
UserDid: currentUser.Did,
65
+
UserDid: currentUser.Active.Did,
66
66
SubjectDid: subjectIdent.DID.String(),
67
67
Rkey: rkey,
68
68
}
···
83
83
return
84
84
case http.MethodDelete:
85
85
// find the record in the db
86
-
follow, err := db.GetFollow(s.db, currentUser.Did, subjectIdent.DID.String())
86
+
follow, err := db.GetFollow(s.db, currentUser.Active.Did, subjectIdent.DID.String())
87
87
if err != nil {
88
88
log.Println("failed to get follow relationship")
89
89
return
···
91
91
92
92
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
93
93
Collection: tangled.GraphFollowNSID,
94
-
Repo: currentUser.Did,
94
+
Repo: currentUser.Active.Did,
95
95
Rkey: follow.Rkey,
96
96
})
97
97
···
100
100
return
101
101
}
102
102
103
-
err = db.DeleteFollowByRkey(s.db, currentUser.Did, follow.Rkey)
103
+
err = db.DeleteFollowByRkey(s.db, currentUser.Active.Did, follow.Rkey)
104
104
if err != nil {
105
105
log.Println("failed to delete follow from DB")
106
106
// this is not an issue, the firehose event might have already done this
+1
-1
appview/state/gfi.go
+1
-1
appview/state/gfi.go
+57
-7
appview/state/login.go
+57
-7
appview/state/login.go
···
5
5
"net/http"
6
6
"strings"
7
7
8
+
"tangled.org/core/appview/oauth"
8
9
"tangled.org/core/appview/pages"
9
10
)
10
11
···
15
16
case http.MethodGet:
16
17
returnURL := r.URL.Query().Get("return_url")
17
18
errorCode := r.URL.Query().Get("error")
19
+
addAccount := r.URL.Query().Get("mode") == "add_account"
20
+
21
+
user := s.oauth.GetMultiAccountUser(r)
22
+
if user == nil {
23
+
registry := s.oauth.GetAccounts(r)
24
+
if len(registry.Accounts) > 0 {
25
+
user = &oauth.MultiAccountUser{
26
+
Active: nil,
27
+
Accounts: registry.Accounts,
28
+
}
29
+
}
30
+
}
18
31
s.pages.Login(w, pages.LoginParams{
19
-
ReturnUrl: returnURL,
20
-
ErrorCode: errorCode,
32
+
ReturnUrl: returnURL,
33
+
ErrorCode: errorCode,
34
+
AddAccount: addAccount,
35
+
LoggedInUser: user,
21
36
})
22
37
case http.MethodPost:
23
38
handle := r.FormValue("handle")
39
+
returnURL := r.FormValue("return_url")
40
+
addAccount := r.FormValue("add_account") == "true"
24
41
25
42
// when users copy their handle from bsky.app, it tends to have these characters around it:
26
43
//
···
44
61
return
45
62
}
46
63
64
+
if err := s.oauth.SetAuthReturn(w, r, returnURL, addAccount); err != nil {
65
+
l.Error("failed to set auth return", "err", err)
66
+
}
67
+
47
68
redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle)
48
69
if err != nil {
49
70
l.Error("failed to start auth", "err", err)
···
58
79
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
59
80
l := s.logger.With("handler", "Logout")
60
81
61
-
err := s.oauth.DeleteSession(w, r)
62
-
if err != nil {
63
-
l.Error("failed to logout", "err", err)
64
-
} else {
65
-
l.Info("logged out successfully")
82
+
currentUser := s.oauth.GetMultiAccountUser(r)
83
+
if currentUser == nil || currentUser.Active == nil {
84
+
s.pages.HxRedirect(w, "/login")
85
+
return
66
86
}
67
87
88
+
currentDid := currentUser.Active.Did
89
+
90
+
var remainingAccounts []string
91
+
for _, acc := range currentUser.Accounts {
92
+
if acc.Did != currentDid {
93
+
remainingAccounts = append(remainingAccounts, acc.Did)
94
+
}
95
+
}
96
+
97
+
if err := s.oauth.RemoveAccount(w, r, currentDid); err != nil {
98
+
l.Error("failed to remove account from registry", "err", err)
99
+
}
100
+
101
+
if err := s.oauth.DeleteSession(w, r); err != nil {
102
+
l.Error("failed to delete session", "err", err)
103
+
}
104
+
105
+
if len(remainingAccounts) > 0 {
106
+
nextDid := remainingAccounts[0]
107
+
if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil {
108
+
l.Error("failed to switch to next account", "err", err)
109
+
s.pages.HxRedirect(w, "/login")
110
+
return
111
+
}
112
+
l.Info("switched to next account after logout", "did", nextDid)
113
+
s.pages.HxRefresh(w)
114
+
return
115
+
}
116
+
117
+
l.Info("logged out last account")
68
118
s.pages.HxRedirect(w, "/login")
69
119
}
+38
-36
appview/state/profile.go
+38
-36
appview/state/profile.go
···
77
77
return nil, fmt.Errorf("failed to get follower stats: %w", err)
78
78
}
79
79
80
-
loggedInUser := s.oauth.GetUser(r)
80
+
loggedInUser := s.oauth.GetMultiAccountUser(r)
81
81
followStatus := models.IsNotFollowing
82
82
if loggedInUser != nil {
83
-
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did)
83
+
followStatus = db.GetFollowStatus(s.db, loggedInUser.Active.Did, did)
84
84
}
85
85
86
86
now := time.Now()
···
163
163
}
164
164
165
165
// populate commit counts in the timeline, using the punchcard
166
-
currentMonth := time.Now().Month()
166
+
now := time.Now()
167
167
for _, p := range profile.Punchcard.Punches {
168
-
idx := currentMonth - p.Date.Month()
169
-
if int(idx) < len(timeline.ByMonth) {
170
-
timeline.ByMonth[idx].Commits += p.Count
168
+
years := now.Year() - p.Date.Year()
169
+
months := int(now.Month() - p.Date.Month())
170
+
monthsAgo := years*12 + months
171
+
if monthsAgo >= 0 && monthsAgo < len(timeline.ByMonth) {
172
+
timeline.ByMonth[monthsAgo].Commits += p.Count
171
173
}
172
174
}
173
175
174
176
s.pages.ProfileOverview(w, pages.ProfileOverviewParams{
175
-
LoggedInUser: s.oauth.GetUser(r),
177
+
LoggedInUser: s.oauth.GetMultiAccountUser(r),
176
178
Card: profile,
177
179
Repos: pinnedRepos,
178
180
CollaboratingRepos: pinnedCollaboratingRepos,
···
203
205
}
204
206
205
207
err = s.pages.ProfileRepos(w, pages.ProfileReposParams{
206
-
LoggedInUser: s.oauth.GetUser(r),
208
+
LoggedInUser: s.oauth.GetMultiAccountUser(r),
207
209
Repos: repos,
208
210
Card: profile,
209
211
})
···
232
234
}
233
235
234
236
err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{
235
-
LoggedInUser: s.oauth.GetUser(r),
237
+
LoggedInUser: s.oauth.GetMultiAccountUser(r),
236
238
Repos: repos,
237
239
Card: profile,
238
240
})
···
257
259
}
258
260
259
261
err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{
260
-
LoggedInUser: s.oauth.GetUser(r),
262
+
LoggedInUser: s.oauth.GetMultiAccountUser(r),
261
263
Strings: strings,
262
264
Card: profile,
263
265
})
···
281
283
}
282
284
l = l.With("profileDid", profile.UserDid)
283
285
284
-
loggedInUser := s.oauth.GetUser(r)
286
+
loggedInUser := s.oauth.GetMultiAccountUser(r)
285
287
params := FollowsPageParams{
286
288
Card: profile,
287
289
}
···
314
316
315
317
loggedInUserFollowing := make(map[string]struct{})
316
318
if loggedInUser != nil {
317
-
following, err := db.GetFollowing(s.db, loggedInUser.Did)
319
+
following, err := db.GetFollowing(s.db, loggedInUser.Active.Did)
318
320
if err != nil {
319
-
l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did)
321
+
l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Active.Did)
320
322
return ¶ms, err
321
323
}
322
324
loggedInUserFollowing = make(map[string]struct{}, len(following))
···
331
333
followStatus := models.IsNotFollowing
332
334
if _, exists := loggedInUserFollowing[did]; exists {
333
335
followStatus = models.IsFollowing
334
-
} else if loggedInUser != nil && loggedInUser.Did == did {
336
+
} else if loggedInUser != nil && loggedInUser.Active.Did == did {
335
337
followStatus = models.IsSelf
336
338
}
337
339
···
365
367
}
366
368
367
369
s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{
368
-
LoggedInUser: s.oauth.GetUser(r),
370
+
LoggedInUser: s.oauth.GetMultiAccountUser(r),
369
371
Followers: followPage.Follows,
370
372
Card: followPage.Card,
371
373
})
···
379
381
}
380
382
381
383
s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{
382
-
LoggedInUser: s.oauth.GetUser(r),
384
+
LoggedInUser: s.oauth.GetMultiAccountUser(r),
383
385
Following: followPage.Follows,
384
386
Card: followPage.Card,
385
387
})
···
528
530
}
529
531
530
532
func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) {
531
-
user := s.oauth.GetUser(r)
533
+
user := s.oauth.GetMultiAccountUser(r)
532
534
533
535
err := r.ParseForm()
534
536
if err != nil {
···
537
539
return
538
540
}
539
541
540
-
profile, err := db.GetProfile(s.db, user.Did)
542
+
profile, err := db.GetProfile(s.db, user.Active.Did)
541
543
if err != nil {
542
-
log.Printf("getting profile data for %s: %s", user.Did, err)
544
+
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
543
545
}
544
546
545
547
profile.Description = r.FormValue("description")
···
576
578
}
577
579
578
580
func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) {
579
-
user := s.oauth.GetUser(r)
581
+
user := s.oauth.GetMultiAccountUser(r)
580
582
581
583
err := r.ParseForm()
582
584
if err != nil {
···
585
587
return
586
588
}
587
589
588
-
profile, err := db.GetProfile(s.db, user.Did)
590
+
profile, err := db.GetProfile(s.db, user.Active.Did)
589
591
if err != nil {
590
-
log.Printf("getting profile data for %s: %s", user.Did, err)
592
+
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
591
593
}
592
594
593
595
i := 0
···
615
617
}
616
618
617
619
func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) {
618
-
user := s.oauth.GetUser(r)
620
+
user := s.oauth.GetMultiAccountUser(r)
619
621
tx, err := s.db.BeginTx(r.Context(), nil)
620
622
if err != nil {
621
623
log.Println("failed to start transaction", err)
···
642
644
vanityStats = append(vanityStats, string(v.Kind))
643
645
}
644
646
645
-
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self")
647
+
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Active.Did, "self")
646
648
var cid *string
647
649
if ex != nil {
648
650
cid = ex.Cid
···
650
652
651
653
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
652
654
Collection: tangled.ActorProfileNSID,
653
-
Repo: user.Did,
655
+
Repo: user.Active.Did,
654
656
Rkey: "self",
655
657
Record: &lexutil.LexiconTypeDecoder{
656
658
Val: &tangled.ActorProfile{
···
679
681
680
682
s.notifier.UpdateProfile(r.Context(), profile)
681
683
682
-
s.pages.HxRedirect(w, "/"+user.Did)
684
+
s.pages.HxRedirect(w, "/"+user.Active.Did)
683
685
}
684
686
685
687
func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) {
686
-
user := s.oauth.GetUser(r)
688
+
user := s.oauth.GetMultiAccountUser(r)
687
689
688
-
profile, err := db.GetProfile(s.db, user.Did)
690
+
profile, err := db.GetProfile(s.db, user.Active.Did)
689
691
if err != nil {
690
-
log.Printf("getting profile data for %s: %s", user.Did, err)
692
+
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
691
693
}
692
694
693
695
s.pages.EditBioFragment(w, pages.EditBioParams{
···
697
699
}
698
700
699
701
func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) {
700
-
user := s.oauth.GetUser(r)
702
+
user := s.oauth.GetMultiAccountUser(r)
701
703
702
-
profile, err := db.GetProfile(s.db, user.Did)
704
+
profile, err := db.GetProfile(s.db, user.Active.Did)
703
705
if err != nil {
704
-
log.Printf("getting profile data for %s: %s", user.Did, err)
706
+
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
705
707
}
706
708
707
-
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did))
709
+
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Active.Did))
708
710
if err != nil {
709
-
log.Printf("getting repos for %s: %s", user.Did, err)
711
+
log.Printf("getting repos for %s: %s", user.Active.Did, err)
710
712
}
711
713
712
-
collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did)
714
+
collaboratingRepos, err := db.CollaboratingIn(s.db, user.Active.Did)
713
715
if err != nil {
714
-
log.Printf("getting collaborating repos for %s: %s", user.Did, err)
716
+
log.Printf("getting collaborating repos for %s: %s", user.Active.Did, err)
715
717
}
716
718
717
719
allRepos := []pages.PinnedRepo{}
+7
-7
appview/state/reaction.go
+7
-7
appview/state/reaction.go
···
17
17
)
18
18
19
19
func (s *State) React(w http.ResponseWriter, r *http.Request) {
20
-
currentUser := s.oauth.GetUser(r)
20
+
currentUser := s.oauth.GetMultiAccountUser(r)
21
21
22
22
subject := r.URL.Query().Get("subject")
23
23
if subject == "" {
···
49
49
rkey := tid.TID()
50
50
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
51
51
Collection: tangled.FeedReactionNSID,
52
-
Repo: currentUser.Did,
52
+
Repo: currentUser.Active.Did,
53
53
Rkey: rkey,
54
54
Record: &lexutil.LexiconTypeDecoder{
55
55
Val: &tangled.FeedReaction{
···
64
64
return
65
65
}
66
66
67
-
err = db.AddReaction(s.db, currentUser.Did, subjectUri, reactionKind, rkey)
67
+
err = db.AddReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind, rkey)
68
68
if err != nil {
69
69
log.Println("failed to react", err)
70
70
return
···
87
87
88
88
return
89
89
case http.MethodDelete:
90
-
reaction, err := db.GetReaction(s.db, currentUser.Did, subjectUri, reactionKind)
90
+
reaction, err := db.GetReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind)
91
91
if err != nil {
92
-
log.Println("failed to get reaction relationship for", currentUser.Did, subjectUri)
92
+
log.Println("failed to get reaction relationship for", currentUser.Active.Did, subjectUri)
93
93
return
94
94
}
95
95
96
96
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
97
97
Collection: tangled.FeedReactionNSID,
98
-
Repo: currentUser.Did,
98
+
Repo: currentUser.Active.Did,
99
99
Rkey: reaction.Rkey,
100
100
})
101
101
···
104
104
return
105
105
}
106
106
107
-
err = db.DeleteReactionByRkey(s.db, currentUser.Did, reaction.Rkey)
107
+
err = db.DeleteReactionByRkey(s.db, currentUser.Active.Did, reaction.Rkey)
108
108
if err != nil {
109
109
log.Println("failed to delete reaction from DB")
110
110
// this is not an issue, the firehose event might have already done this
+5
appview/state/router.go
+5
appview/state/router.go
···
109
109
})
110
110
111
111
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
112
+
w.WriteHeader(http.StatusNotFound)
112
113
s.pages.Error404(w)
113
114
})
114
115
···
130
131
r.Get("/login", s.Login)
131
132
r.Post("/login", s.Login)
132
133
r.Post("/logout", s.Logout)
134
+
135
+
r.Post("/account/switch", s.SwitchAccount)
136
+
r.With(middleware.AuthMiddleware(s.oauth)).Delete("/account/{did}", s.RemoveAccount)
133
137
134
138
r.Route("/repo", func(r chi.Router) {
135
139
r.Route("/new", func(r chi.Router) {
···
182
186
r.Get("/brand", s.Brand)
183
187
184
188
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
189
+
w.WriteHeader(http.StatusNotFound)
185
190
s.pages.Error404(w)
186
191
})
187
192
return r
+6
-6
appview/state/star.go
+6
-6
appview/state/star.go
···
16
16
)
17
17
18
18
func (s *State) Star(w http.ResponseWriter, r *http.Request) {
19
-
currentUser := s.oauth.GetUser(r)
19
+
currentUser := s.oauth.GetMultiAccountUser(r)
20
20
21
21
subject := r.URL.Query().Get("subject")
22
22
if subject == "" {
···
42
42
rkey := tid.TID()
43
43
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
44
44
Collection: tangled.FeedStarNSID,
45
-
Repo: currentUser.Did,
45
+
Repo: currentUser.Active.Did,
46
46
Rkey: rkey,
47
47
Record: &lexutil.LexiconTypeDecoder{
48
48
Val: &tangled.FeedStar{
···
57
57
log.Println("created atproto record: ", resp.Uri)
58
58
59
59
star := &models.Star{
60
-
Did: currentUser.Did,
60
+
Did: currentUser.Active.Did,
61
61
RepoAt: subjectUri,
62
62
Rkey: rkey,
63
63
}
···
84
84
return
85
85
case http.MethodDelete:
86
86
// find the record in the db
87
-
star, err := db.GetStar(s.db, currentUser.Did, subjectUri)
87
+
star, err := db.GetStar(s.db, currentUser.Active.Did, subjectUri)
88
88
if err != nil {
89
89
log.Println("failed to get star relationship")
90
90
return
···
92
92
93
93
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
94
94
Collection: tangled.FeedStarNSID,
95
-
Repo: currentUser.Did,
95
+
Repo: currentUser.Active.Did,
96
96
Rkey: star.Rkey,
97
97
})
98
98
···
101
101
return
102
102
}
103
103
104
-
err = db.DeleteStarByRkey(s.db, currentUser.Did, star.Rkey)
104
+
err = db.DeleteStarByRkey(s.db, currentUser.Active.Did, star.Rkey)
105
105
if err != nil {
106
106
log.Println("failed to delete star from DB")
107
107
// this is not an issue, the firehose event might have already done this
+22
-22
appview/state/state.go
+22
-22
appview/state/state.go
···
249
249
}
250
250
251
251
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
252
-
user := s.oauth.GetUser(r)
252
+
user := s.oauth.GetMultiAccountUser(r)
253
253
s.pages.TermsOfService(w, pages.TermsOfServiceParams{
254
254
LoggedInUser: user,
255
255
})
256
256
}
257
257
258
258
func (s *State) PrivacyPolicy(w http.ResponseWriter, r *http.Request) {
259
-
user := s.oauth.GetUser(r)
259
+
user := s.oauth.GetMultiAccountUser(r)
260
260
s.pages.PrivacyPolicy(w, pages.PrivacyPolicyParams{
261
261
LoggedInUser: user,
262
262
})
263
263
}
264
264
265
265
func (s *State) Brand(w http.ResponseWriter, r *http.Request) {
266
-
user := s.oauth.GetUser(r)
266
+
user := s.oauth.GetMultiAccountUser(r)
267
267
s.pages.Brand(w, pages.BrandParams{
268
268
LoggedInUser: user,
269
269
})
270
270
}
271
271
272
272
func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) {
273
-
if s.oauth.GetUser(r) != nil {
273
+
if s.oauth.GetMultiAccountUser(r) != nil {
274
274
s.Timeline(w, r)
275
275
return
276
276
}
···
278
278
}
279
279
280
280
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
281
-
user := s.oauth.GetUser(r)
281
+
user := s.oauth.GetMultiAccountUser(r)
282
282
283
283
// TODO: set this flag based on the UI
284
284
filtered := false
285
285
286
286
var userDid string
287
-
if user != nil {
288
-
userDid = user.Did
287
+
if user != nil && user.Active != nil {
288
+
userDid = user.Active.Did
289
289
}
290
290
timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered)
291
291
if err != nil {
···
314
314
}
315
315
316
316
func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) {
317
-
user := s.oauth.GetUser(r)
317
+
user := s.oauth.GetMultiAccountUser(r)
318
318
if user == nil {
319
319
return
320
320
}
321
321
322
322
l := s.logger.With("handler", "UpgradeBanner")
323
-
l = l.With("did", user.Did)
323
+
l = l.With("did", user.Active.Did)
324
324
325
325
regs, err := db.GetRegistrations(
326
326
s.db,
327
-
orm.FilterEq("did", user.Did),
327
+
orm.FilterEq("did", user.Active.Did),
328
328
orm.FilterEq("needs_upgrade", 1),
329
329
)
330
330
if err != nil {
···
333
333
334
334
spindles, err := db.GetSpindles(
335
335
s.db,
336
-
orm.FilterEq("owner", user.Did),
336
+
orm.FilterEq("owner", user.Active.Did),
337
337
orm.FilterEq("needs_upgrade", 1),
338
338
)
339
339
if err != nil {
···
447
447
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
448
448
switch r.Method {
449
449
case http.MethodGet:
450
-
user := s.oauth.GetUser(r)
451
-
knots, err := s.enforcer.GetKnotsForUser(user.Did)
450
+
user := s.oauth.GetMultiAccountUser(r)
451
+
knots, err := s.enforcer.GetKnotsForUser(user.Active.Did)
452
452
if err != nil {
453
453
s.pages.Notice(w, "repo", "Invalid user account.")
454
454
return
···
462
462
case http.MethodPost:
463
463
l := s.logger.With("handler", "NewRepo")
464
464
465
-
user := s.oauth.GetUser(r)
466
-
l = l.With("did", user.Did)
465
+
user := s.oauth.GetMultiAccountUser(r)
466
+
l = l.With("did", user.Active.Did)
467
467
468
468
// form validation
469
469
domain := r.FormValue("domain")
···
495
495
description := r.FormValue("description")
496
496
497
497
// ACL validation
498
-
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
498
+
ok, err := s.enforcer.E.Enforce(user.Active.Did, domain, domain, "repo:create")
499
499
if err != nil || !ok {
500
500
l.Info("unauthorized")
501
501
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
···
505
505
// Check for existing repos
506
506
existingRepo, err := db.GetRepo(
507
507
s.db,
508
-
orm.FilterEq("did", user.Did),
508
+
orm.FilterEq("did", user.Active.Did),
509
509
orm.FilterEq("name", repoName),
510
510
)
511
511
if err == nil && existingRepo != nil {
···
517
517
// create atproto record for this repo
518
518
rkey := tid.TID()
519
519
repo := &models.Repo{
520
-
Did: user.Did,
520
+
Did: user.Active.Did,
521
521
Name: repoName,
522
522
Knot: domain,
523
523
Rkey: rkey,
···
536
536
537
537
atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{
538
538
Collection: tangled.RepoNSID,
539
-
Repo: user.Did,
539
+
Repo: user.Active.Did,
540
540
Rkey: rkey,
541
541
Record: &lexutil.LexiconTypeDecoder{
542
542
Val: &record,
···
613
613
}
614
614
615
615
// acls
616
-
p, _ := securejoin.SecureJoin(user.Did, repoName)
617
-
err = s.enforcer.AddRepo(user.Did, domain, p)
616
+
p, _ := securejoin.SecureJoin(user.Active.Did, repoName)
617
+
err = s.enforcer.AddRepo(user.Active.Did, domain, p)
618
618
if err != nil {
619
619
l.Error("acl setup failed", "err", err)
620
620
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
639
639
aturi = ""
640
640
641
641
s.notifier.NewRepo(r.Context(), repo)
642
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, repoName))
642
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName))
643
643
}
644
644
}
645
645
+19
-19
appview/strings/strings.go
+19
-19
appview/strings/strings.go
···
82
82
}
83
83
84
84
s.Pages.StringsTimeline(w, pages.StringTimelineParams{
85
-
LoggedInUser: s.OAuth.GetUser(r),
85
+
LoggedInUser: s.OAuth.GetMultiAccountUser(r),
86
86
Strings: strings,
87
87
})
88
88
}
···
153
153
if err != nil {
154
154
l.Error("failed to get star count", "err", err)
155
155
}
156
-
user := s.OAuth.GetUser(r)
156
+
user := s.OAuth.GetMultiAccountUser(r)
157
157
isStarred := false
158
158
if user != nil {
159
-
isStarred = db.GetStarStatus(s.Db, user.Did, string.AtUri())
159
+
isStarred = db.GetStarStatus(s.Db, user.Active.Did, string.AtUri())
160
160
}
161
161
162
162
s.Pages.SingleString(w, pages.SingleStringParams{
···
178
178
func (s *Strings) edit(w http.ResponseWriter, r *http.Request) {
179
179
l := s.Logger.With("handler", "edit")
180
180
181
-
user := s.OAuth.GetUser(r)
181
+
user := s.OAuth.GetMultiAccountUser(r)
182
182
183
183
id, ok := r.Context().Value("resolvedId").(identity.Identity)
184
184
if !ok {
···
216
216
first := all[0]
217
217
218
218
// verify that the logged in user owns this string
219
-
if user.Did != id.DID.String() {
220
-
l.Error("unauthorized request", "expected", id.DID, "got", user.Did)
219
+
if user.Active.Did != id.DID.String() {
220
+
l.Error("unauthorized request", "expected", id.DID, "got", user.Active.Did)
221
221
w.WriteHeader(http.StatusUnauthorized)
222
222
return
223
223
}
···
226
226
case http.MethodGet:
227
227
// return the form with prefilled fields
228
228
s.Pages.PutString(w, pages.PutStringParams{
229
-
LoggedInUser: s.OAuth.GetUser(r),
229
+
LoggedInUser: s.OAuth.GetMultiAccountUser(r),
230
230
Action: "edit",
231
231
String: first,
232
232
})
···
299
299
s.Notifier.EditString(r.Context(), &entry)
300
300
301
301
// if that went okay, redir to the string
302
-
s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey)
302
+
s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+entry.Rkey)
303
303
}
304
304
305
305
}
306
306
307
307
func (s *Strings) create(w http.ResponseWriter, r *http.Request) {
308
308
l := s.Logger.With("handler", "create")
309
-
user := s.OAuth.GetUser(r)
309
+
user := s.OAuth.GetMultiAccountUser(r)
310
310
311
311
switch r.Method {
312
312
case http.MethodGet:
313
313
s.Pages.PutString(w, pages.PutStringParams{
314
-
LoggedInUser: s.OAuth.GetUser(r),
314
+
LoggedInUser: s.OAuth.GetMultiAccountUser(r),
315
315
Action: "new",
316
316
})
317
317
case http.MethodPost:
···
335
335
description := r.FormValue("description")
336
336
337
337
string := models.String{
338
-
Did: syntax.DID(user.Did),
338
+
Did: syntax.DID(user.Active.Did),
339
339
Rkey: tid.TID(),
340
340
Filename: filename,
341
341
Description: description,
···
353
353
354
354
resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{
355
355
Collection: tangled.StringNSID,
356
-
Repo: user.Did,
356
+
Repo: user.Active.Did,
357
357
Rkey: string.Rkey,
358
358
Record: &lexutil.LexiconTypeDecoder{
359
359
Val: &record,
···
375
375
s.Notifier.NewString(r.Context(), &string)
376
376
377
377
// successful
378
-
s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey)
378
+
s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+string.Rkey)
379
379
}
380
380
}
381
381
382
382
func (s *Strings) delete(w http.ResponseWriter, r *http.Request) {
383
383
l := s.Logger.With("handler", "create")
384
-
user := s.OAuth.GetUser(r)
384
+
user := s.OAuth.GetMultiAccountUser(r)
385
385
fail := func(msg string, err error) {
386
386
l.Error(msg, "err", err)
387
387
s.Pages.Notice(w, "error", msg)
···
402
402
return
403
403
}
404
404
405
-
if user.Did != id.DID.String() {
406
-
fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String()))
405
+
if user.Active.Did != id.DID.String() {
406
+
fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Active.Did, id.DID.String()))
407
407
return
408
408
}
409
409
410
410
if err := db.DeleteString(
411
411
s.Db,
412
-
orm.FilterEq("did", user.Did),
412
+
orm.FilterEq("did", user.Active.Did),
413
413
orm.FilterEq("rkey", rkey),
414
414
); err != nil {
415
415
fail("Failed to delete string.", err)
416
416
return
417
417
}
418
418
419
-
s.Notifier.DeleteString(r.Context(), user.Did, rkey)
419
+
s.Notifier.DeleteString(r.Context(), user.Active.Did, rkey)
420
420
421
-
s.Pages.HxRedirect(w, "/strings/"+user.Did)
421
+
s.Pages.HxRedirect(w, "/strings/"+user.Active.Did)
422
422
}
423
423
424
424
func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {
+86
-89
docs/DOCS.md
+86
-89
docs/DOCS.md
···
1
1
---
2
-
title: Tangled Documentation
2
+
title: Tangled docs
3
3
author: The Tangled Contributors
4
4
date: 21 Sun, Dec 2025
5
-
---
6
-
7
-
# Introduction
8
-
9
-
Tangled is a decentralized code hosting and collaboration
10
-
platform. Every component of Tangled is open-source and
11
-
selfhostable. [tangled.org](https://tangled.org) also
12
-
provides hosting and CI services that are free to use.
5
+
abstract: |
6
+
Tangled is a decentralized code hosting and collaboration
7
+
platform. Every component of Tangled is open-source and
8
+
self-hostable. [tangled.org](https://tangled.org) also
9
+
provides hosting and CI services that are free to use.
13
10
14
-
There are several models for decentralized code
15
-
collaboration platforms, ranging from ActivityPubโs
16
-
(Forgejo) federated model, to Radicleโs entirely P2P model.
17
-
Our approach attempts to be the best of both worlds by
18
-
adopting atprotoโa protocol for building decentralized
19
-
social applications with a central identity
11
+
There are several models for decentralized code
12
+
collaboration platforms, ranging from ActivityPubโs
13
+
(Forgejo) federated model, to Radicleโs entirely P2P model.
14
+
Our approach attempts to be the best of both worlds by
15
+
adopting the AT Protocolโa protocol for building decentralized
16
+
social applications with a central identity
20
17
21
-
Our approach to this is the idea of โknotsโ. Knots are
22
-
lightweight, headless servers that enable users to host Git
23
-
repositories with ease. Knots are designed for either single
24
-
or multi-tenant use which is perfect for self-hosting on a
25
-
Raspberry Pi at home, or larger โcommunityโ servers. By
26
-
default, Tangled provides managed knots where you can host
27
-
your repositories for free.
18
+
Our approach to this is the idea of โknotsโ. Knots are
19
+
lightweight, headless servers that enable users to host Git
20
+
repositories with ease. Knots are designed for either single
21
+
or multi-tenant use which is perfect for self-hosting on a
22
+
Raspberry Pi at home, or larger โcommunityโ servers. By
23
+
default, Tangled provides managed knots where you can host
24
+
your repositories for free.
28
25
29
-
The "appview" at tangled.org acts as a consolidated โviewโ
30
-
into the whole network, allowing users to access, clone and
31
-
contribute to repositories hosted across different knots
32
-
seamlessly.
26
+
The appview at tangled.org acts as a consolidated "view"
27
+
into the whole network, allowing users to access, clone and
28
+
contribute to repositories hosted across different knots
29
+
seamlessly.
30
+
---
33
31
34
-
# Quick Start Guide
32
+
# Quick start guide
35
33
36
-
## Login or Sign up
34
+
## Login or sign up
37
35
38
-
You can [login](https://tangled.org) by using your AT
36
+
You can [login](https://tangled.org) by using your AT Protocol
39
37
account. If you are unclear on what that means, simply head
40
38
to the [signup](https://tangled.org/signup) page and create
41
39
an account. By doing so, you will be choosing Tangled as
42
40
your account provider (you will be granted a handle of the
43
41
form `user.tngl.sh`).
44
42
45
-
In the AT network, users are free to choose their account
43
+
In the AT Protocol network, users are free to choose their account
46
44
provider (known as a "Personal Data Service", or PDS), and
47
45
login to applications that support AT accounts.
48
46
49
-
You can think of it as "one account for all of the
50
-
atmosphere"!
47
+
You can think of it as "one account for all of the atmosphere"!
51
48
52
49
If you already have an AT account (you may have one if you
53
50
signed up to Bluesky, for example), you can login with the
54
51
same handle on Tangled (so just use `user.bsky.social` on
55
52
the login page).
56
53
57
-
## Add an SSH Key
54
+
## Add an SSH key
58
55
59
56
Once you are logged in, you can start creating repositories
60
57
and pushing code. Tangled supports pushing git repositories
···
87
84
paste your public key, give it a descriptive name, and hit
88
85
save.
89
86
90
-
## Create a Repository
87
+
## Create a repository
91
88
92
89
Once your SSH key is added, create your first repository:
93
90
···
98
95
4. Choose a knotserver to host this repository on
99
96
5. Hit create
100
97
101
-
"Knots" are selfhostable, lightweight git servers that can
98
+
Knots are self-hostable, lightweight Git servers that can
102
99
host your repository. Unlike traditional code forges, your
103
100
code can live on any server. Read the [Knots](TODO) section
104
101
for more.
···
125
122
are hosted by tangled.org. If you use a custom knot, refer
126
123
to the [Knots](TODO) section.
127
124
128
-
## Push Your First Repository
125
+
## Push your first repository
129
126
130
-
Initialize a new git repository:
127
+
Initialize a new Git repository:
131
128
132
129
```bash
133
130
mkdir my-project
···
165
162
cd /path/to/your/existing/repo
166
163
```
167
164
168
-
You can inspect your existing git remote like so:
165
+
You can inspect your existing Git remote like so:
169
166
170
167
```bash
171
168
git remote -v
···
197
194
origin git@tangled.org:user.tngl.sh/my-project (push)
198
195
```
199
196
200
-
Push all your branches and tags to tangled:
197
+
Push all your branches and tags to Tangled:
201
198
202
199
```bash
203
200
git push -u origin --all
···
232
229
```
233
230
234
231
You also need to re-add the original URL as a push
235
-
destination (git replaces the push URL when you use `--add`
232
+
destination (Git replaces the push URL when you use `--add`
236
233
the first time):
237
234
238
235
```bash
···
249
246
```
250
247
251
248
Notice that there's one fetch URL (the primary remote) and
252
-
two push URLs. Now, whenever you push, git will
249
+
two push URLs. Now, whenever you push, Git will
253
250
automatically push to both remotes:
254
251
255
252
```bash
···
301
298
## Docker
302
299
303
300
Refer to
304
-
[@tangled.org/knot-docker](https://tangled.sh/@tangled.sh/knot-docker).
301
+
[@tangled.org/knot-docker](https://tangled.org/@tangled.org/knot-docker).
305
302
Note that this is community maintained.
306
303
307
304
## Manual setup
···
372
369
```
373
370
KNOT_REPO_SCAN_PATH=/home/git
374
371
KNOT_SERVER_HOSTNAME=knot.example.com
375
-
APPVIEW_ENDPOINT=https://tangled.sh
372
+
APPVIEW_ENDPOINT=https://tangled.org
376
373
KNOT_SERVER_OWNER=did:plc:foobar
377
374
KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444
378
375
KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555
···
603
600
- `nixery`: This uses an instance of
604
601
[Nixery](https://nixery.dev) to run steps, which allows
605
602
you to add [dependencies](#dependencies) from
606
-
[Nixpkgs](https://github.com/NixOS/nixpkgs). You can
603
+
Nixpkgs (https://github.com/NixOS/nixpkgs). You can
607
604
search for packages on https://search.nixos.org, and
608
605
there's a pretty good chance the package(s) you're looking
609
606
for will be there.
···
630
627
default, the depth is set to 1, meaning only the most
631
628
recent commit will be fetched, which is the commit that
632
629
triggered the workflow.
633
-
- `submodules`: If you use [git
634
-
submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
630
+
- `submodules`: If you use Git submodules
631
+
(https://git-scm.com/book/en/v2/Git-Tools-Submodules)
635
632
in your repository, setting this field to `true` will
636
633
recursively fetch all submodules. This is `false` by
637
634
default.
···
657
654
Say you want to fetch Node.js and Go from `nixpkgs`, and a
658
655
package called `my_pkg` you've made from your own registry
659
656
at your repository at
660
-
`https://tangled.sh/@example.com/my_pkg`. You can define
657
+
`https://tangled.org/@example.com/my_pkg`. You can define
661
658
those dependencies like so:
662
659
663
660
```yaml
···
779
776
780
777
If you want another example of a workflow, you can look at
781
778
the one [Tangled uses to build the
782
-
project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml).
779
+
project](https://tangled.org/@tangled.org/core/blob/master/.tangled/workflows/build.yml).
783
780
784
781
## Self-hosting guide
785
782
···
836
833
837
834
## Architecture
838
835
839
-
Spindle is a small CI runner service. Here's a high level overview of how it operates:
836
+
Spindle is a small CI runner service. Here's a high-level overview of how it operates:
840
837
841
-
* listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and
838
+
* Listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and
842
839
[`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream.
843
-
* when a new repo record comes through (typically when you add a spindle to a
840
+
* When a new repo record comes through (typically when you add a spindle to a
844
841
repo from the settings), spindle then resolves the underlying knot and
845
842
subscribes to repo events (see:
846
843
[`sh.tangled.pipeline`](/lexicons/pipeline.json)).
847
-
* the spindle engine then handles execution of the pipeline, with results and
848
-
logs beamed on the spindle event stream over wss
844
+
* The spindle engine then handles execution of the pipeline, with results and
845
+
logs beamed on the spindle event stream over WebSocket
849
846
850
847
### The engine
851
848
852
849
At present, the only supported backend is Docker (and Podman, if Docker
853
-
compatibility is enabled, so that `/run/docker.sock` is created). Spindle
850
+
compatibility is enabled, so that `/run/docker.sock` is created). spindle
854
851
executes each step in the pipeline in a fresh container, with state persisted
855
852
across steps within the `/tangled/workspace` directory.
856
853
···
858
855
[Nixery](https://nixery.dev), which is handy for caching layers for frequently
859
856
used packages.
860
857
861
-
The pipeline manifest is [specified here](/docs/spindle/pipeline.md).
858
+
The pipeline manifest is [specified here](https://docs.tangled.org/spindles.html#pipelines).
862
859
863
860
## Secrets with openbao
864
861
865
-
This document covers setting up Spindle to use OpenBao for secrets
862
+
This document covers setting up spindle to use OpenBao for secrets
866
863
management via OpenBao Proxy instead of the default SQLite backend.
867
864
868
865
### Overview
869
866
870
867
Spindle now uses OpenBao Proxy for secrets management. The proxy handles
871
-
authentication automatically using AppRole credentials, while Spindle
868
+
authentication automatically using AppRole credentials, while spindle
872
869
connects to the local proxy instead of directly to the OpenBao server.
873
870
874
871
This approach provides better security, automatic token renewal, and
···
876
873
877
874
### Installation
878
875
879
-
Install OpenBao from nixpkgs:
876
+
Install OpenBao from Nixpkgs:
880
877
881
878
```bash
882
879
nix shell nixpkgs#openbao # for a local server
···
1029
1026
}
1030
1027
}
1031
1028
1032
-
# Proxy listener for Spindle
1029
+
# Proxy listener for spindle
1033
1030
listener "tcp" {
1034
1031
address = "127.0.0.1:8201"
1035
1032
tls_disable = true
···
1062
1059
1063
1060
#### Configure spindle
1064
1061
1065
-
Set these environment variables for Spindle:
1062
+
Set these environment variables for spindle:
1066
1063
1067
1064
```bash
1068
1065
export SPINDLE_SERVER_SECRETS_PROVIDER=openbao
···
1070
1067
export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle
1071
1068
```
1072
1069
1073
-
On startup, the spindle will now connect to the local proxy,
1070
+
On startup, spindle will now connect to the local proxy,
1074
1071
which handles all authentication automatically.
1075
1072
1076
1073
### Production setup for proxy
···
1099
1096
# List all secrets
1100
1097
bao kv list spindle/
1101
1098
1102
-
# Add a test secret via Spindle API, then check it exists
1099
+
# Add a test secret via the spindle API, then check it exists
1103
1100
bao kv list spindle/repos/
1104
1101
1105
1102
# Get a specific secret
···
1112
1109
port 8200 or 8201)
1113
1110
- The proxy authenticates with OpenBao using AppRole
1114
1111
credentials
1115
-
- All Spindle requests go through the proxy, which injects
1112
+
- All spindle requests go through the proxy, which injects
1116
1113
authentication tokens
1117
1114
- Secrets are stored at
1118
1115
`spindle/repos/{sanitized_repo_path}/{secret_key}`
···
1131
1128
and the policy has the necessary permissions.
1132
1129
1133
1130
**404 route errors**: The spindle KV mount probably doesn't
1134
-
exist - run the mount creation step again.
1131
+
existโrun the mount creation step again.
1135
1132
1136
1133
**Proxy authentication failures**: Check the proxy logs and
1137
1134
verify the role-id and secret-id files are readable and
···
1159
1156
secret_id="$(cat /tmp/openbao/secret-id)"
1160
1157
```
1161
1158
1162
-
# Migrating knots & spindles
1159
+
# Migrating knots and spindles
1163
1160
1164
1161
Sometimes, non-backwards compatible changes are made to the
1165
1162
knot/spindle XRPC APIs. If you host a knot or a spindle, you
···
1172
1169
1173
1170
## Upgrading from v1.8.x
1174
1171
1175
-
After v1.8.2, the HTTP API for knot and spindles have been
1172
+
After v1.8.2, the HTTP API for knots and spindles has been
1176
1173
deprecated and replaced with XRPC. Repositories on outdated
1177
1174
knots will not be viewable from the appview. Upgrading is
1178
1175
straightforward however.
1179
1176
1180
1177
For knots:
1181
1178
1182
-
- Upgrade to latest tag (v1.9.0 or above)
1179
+
- Upgrade to the latest tag (v1.9.0 or above)
1183
1180
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
1184
1181
hit the "retry" button to verify your knot
1185
1182
1186
1183
For spindles:
1187
1184
1188
-
- Upgrade to latest tag (v1.9.0 or above)
1185
+
- Upgrade to the latest tag (v1.9.0 or above)
1189
1186
- Head to the [spindle
1190
1187
dashboard](https://tangled.org/settings/spindles) and hit the
1191
1188
"retry" button to verify your spindle
···
1227
1224
# Hacking on Tangled
1228
1225
1229
1226
We highly recommend [installing
1230
-
nix](https://nixos.org/download/) (the package manager)
1231
-
before working on the codebase. The nix flake provides a lot
1227
+
Nix](https://nixos.org/download/) (the package manager)
1228
+
before working on the codebase. The Nix flake provides a lot
1232
1229
of helpers to get started and most importantly, builds and
1233
1230
dev shells are entirely deterministic.
1234
1231
···
1238
1235
nix develop
1239
1236
```
1240
1237
1241
-
Non-nix users can look at the `devShell` attribute in the
1238
+
Non-Nix users can look at the `devShell` attribute in the
1242
1239
`flake.nix` file to determine necessary dependencies.
1243
1240
1244
1241
## Running the appview
1245
1242
1246
-
The nix flake also exposes a few `app` attributes (run `nix
1243
+
The Nix flake also exposes a few `app` attributes (run `nix
1247
1244
flake show` to see a full list of what the flake provides),
1248
1245
one of the apps runs the appview with the `air`
1249
1246
live-reloader:
···
1258
1255
nix run .#watch-tailwind
1259
1256
```
1260
1257
1261
-
To authenticate with the appview, you will need redis and
1262
-
OAUTH JWKs to be setup:
1258
+
To authenticate with the appview, you will need Redis and
1259
+
OAuth JWKs to be set up:
1263
1260
1264
1261
```
1265
-
# oauth jwks should already be setup by the nix devshell:
1262
+
# OAuth JWKs should already be set up by the Nix devshell:
1266
1263
echo $TANGLED_OAUTH_CLIENT_SECRET
1267
1264
z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc
1268
1265
···
1280
1277
# the secret key from above
1281
1278
export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..."
1282
1279
1283
-
# run redis in at a new shell to store oauth sessions
1280
+
# Run Redis in a new shell to store OAuth sessions
1284
1281
redis-server
1285
1282
```
1286
1283
1287
1284
## Running knots and spindles
1288
1285
1289
1286
An end-to-end knot setup requires setting up a machine with
1290
-
`sshd`, `AuthorizedKeysCommand`, and git user, which is
1291
-
quite cumbersome. So the nix flake provides a
1287
+
`sshd`, `AuthorizedKeysCommand`, and a Git user, which is
1288
+
quite cumbersome. So the Nix flake provides a
1292
1289
`nixosConfiguration` to do so.
1293
1290
1294
1291
<details>
1295
-
<summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary>
1292
+
<summary><strong>macOS users will have to set up a Nix Builder first</strong></summary>
1296
1293
1297
1294
In order to build Tangled's dev VM on macOS, you will
1298
1295
first need to set up a Linux Nix builder. The recommended
···
1303
1300
you are using Apple Silicon).
1304
1301
1305
1302
> IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside
1306
-
> the tangled repo so that it doesn't conflict with the other VM. For example,
1303
+
> the Tangled repo so that it doesn't conflict with the other VM. For example,
1307
1304
> you can do
1308
1305
>
1309
1306
> ```shell
···
1316
1313
> avoid subtle problems.
1317
1314
1318
1315
Alternatively, you can use any other method to set up a
1319
-
Linux machine with `nix` installed that you can `sudo ssh`
1316
+
Linux machine with Nix installed that you can `sudo ssh`
1320
1317
into (in other words, root user on your Mac has to be able
1321
1318
to ssh into the Linux machine without entering a password)
1322
1319
and that has the same architecture as your Mac. See
···
1347
1344
with `ssh` exposed on port 2222.
1348
1345
1349
1346
Once the services are running, head to
1350
-
http://localhost:3000/settings/knots and hit verify. It should
1347
+
http://localhost:3000/settings/knots and hit "Verify". It should
1351
1348
verify the ownership of the services instantly if everything
1352
1349
went smoothly.
1353
1350
···
1371
1368
1372
1369
The above VM should already be running a spindle on
1373
1370
`localhost:6555`. Head to http://localhost:3000/settings/spindles and
1374
-
hit verify. You can then configure each repository to use
1371
+
hit "Verify". You can then configure each repository to use
1375
1372
this spindle and run CI jobs.
1376
1373
1377
1374
Of interest when debugging spindles:
1378
1375
1379
1376
```
1380
-
# service logs from journald:
1377
+
# Service logs from journald:
1381
1378
journalctl -xeu spindle
1382
1379
1383
1380
# CI job logs from disk:
1384
1381
ls /var/log/spindle
1385
1382
1386
-
# debugging spindle db:
1383
+
# Debugging spindle database:
1387
1384
sqlite3 /var/lib/spindle/spindle.db
1388
1385
1389
1386
# litecli has a nicer REPL interface:
···
1432
1429
1433
1430
### General notes
1434
1431
1435
-
- PRs get merged "as-is" (fast-forward) -- like applying a patch-series
1436
-
using `git am`. At present, there is no squashing -- so please author
1432
+
- PRs get merged "as-is" (fast-forward)โlike applying a patch-series
1433
+
using `git am`. At present, there is no squashingโso please author
1437
1434
your commits as they would appear on `master`, following the above
1438
1435
guidelines.
1439
1436
- If there is a lot of nesting, for example "appview:
···
1454
1451
## Code formatting
1455
1452
1456
1453
We use a variety of tools to format our code, and multiplex them with
1457
-
[`treefmt`](https://treefmt.com): all you need to do to format your changes
1454
+
[`treefmt`](https://treefmt.com). All you need to do to format your changes
1458
1455
is run `nix run .#fmt` (or just `treefmt` if you're in the devshell).
1459
1456
1460
1457
## Proposals for bigger changes
···
1482
1479
We'll use the issue thread to discuss and refine the idea before moving
1483
1480
forward.
1484
1481
1485
-
## Developer certificate of origin (DCO)
1482
+
## Developer Certificate of Origin (DCO)
1486
1483
1487
1484
We require all contributors to certify that they have the right to
1488
1485
submit the code they're contributing. To do this, we follow the
+3
docs/mode.html
+3
docs/mode.html
+7
docs/search.html
+7
docs/search.html
···
1
+
<form action="https://google.com/search" role="search" aria-label="Sitewide" class="w-full">
2
+
<input type="hidden" name="q" value="+[inurl:https://docs.tangled.org]">
3
+
<label>
4
+
<span style="display:none;">Search</span>
5
+
<input type="text" name="q" placeholder="Search docs ..." class="w-full font-normal">
6
+
</label>
7
+
</form>
+80
-41
docs/template.html
+80
-41
docs/template.html
···
20
20
<meta name="description" content="$description-meta$" />
21
21
$endif$
22
22
23
-
<title>$pagetitle$ - Tangled docs</title>
23
+
<title>$pagetitle$</title>
24
24
25
25
<style>
26
26
$styles.css()$
···
37
37
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
38
38
39
39
</head>
40
-
<body class="bg-white dark:bg-gray-900 min-h-screen">
40
+
<body class="bg-white dark:bg-gray-900 flex flex-col min-h-svh">
41
41
$for(include-before)$
42
42
$include-before$
43
43
$endfor$
44
+
44
45
$if(toc)$
45
-
<!-- mobile topbar toc -->
46
-
<details id="mobile-$idprefix$TOC" role="doc-toc" class="md:hidden bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 z-50 space-y-4 group px-6 py-4">
47
-
<summary class="cursor-pointer list-none text-sm font-semibold select-none flex gap-2 justify-between items-center dark:text-white">
48
-
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
49
-
<span class="group-open:hidden inline">${ menu.svg() }</span>
50
-
<span class="hidden group-open:inline">${ x.svg() }</span>
51
-
</summary>
52
-
${ table-of-contents:toc.html() }
53
-
</details>
46
+
<!-- mobile TOC trigger -->
47
+
<div class="md:hidden px-6 py-4 border-b border-gray-200 dark:border-gray-700">
48
+
<button
49
+
type="button"
50
+
popovertarget="mobile-toc-popover"
51
+
popovertargetaction="toggle"
52
+
class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white"
53
+
>
54
+
${ menu.svg() }
55
+
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
56
+
</button>
57
+
</div>
58
+
59
+
<div
60
+
id="mobile-toc-popover"
61
+
popover
62
+
class="mobile-toc-popover
63
+
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
64
+
h-full overflow-y-auto shadow-sm
65
+
px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0"
66
+
>
67
+
<div class="flex flex-col min-h-full">
68
+
<div class="flex-1 space-y-4">
69
+
<button
70
+
type="button"
71
+
popovertarget="mobile-toc-popover"
72
+
popovertargetaction="toggle"
73
+
class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4">
74
+
${ x.svg() }
75
+
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
76
+
</button>
77
+
${ search.html() }
78
+
${ table-of-contents:toc.html() }
79
+
</div>
80
+
${ single-page:mode.html() }
81
+
</div>
82
+
</div>
54
83
55
-
<!-- desktop sidebar toc -->
56
-
<nav id="$idprefix$TOC" role="doc-toc" class="hidden md:block fixed left-0 top-0 w-80 h-screen bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-4 z-50">
57
-
$if(toc-title)$
58
-
<h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2>
59
-
$endif$
60
-
${ table-of-contents:toc.html() }
61
-
</nav>
84
+
<!-- desktop sidebar toc -->
85
+
<nav
86
+
id="$idprefix$TOC"
87
+
role="doc-toc"
88
+
class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen
89
+
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
90
+
p-4 z-50 overflow-y-auto">
91
+
${ search.html() }
92
+
<div class="flex-1">
93
+
$if(toc-title)$
94
+
<h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2>
95
+
$endif$
96
+
${ table-of-contents:toc.html() }
97
+
</div>
98
+
${ single-page:mode.html() }
99
+
</nav>
62
100
$endif$
63
101
64
-
<div class="min-h-screen flex-1 flex flex-col $if(toc)$md:ml-80$endif$">
65
-
<main class="flex-1 max-w-4xl w-full mx-auto p-6">
102
+
<div class="$if(toc)$md:ml-80$endif$ flex-1 flex flex-col">
103
+
<main class="max-w-4xl w-full mx-auto p-6 flex-1">
66
104
$if(top)$
67
-
$-- only print title block if this is NOT the top page
105
+
$-- only print title block if this is NOT the top page
68
106
$else$
69
107
$if(title)$
70
-
<header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700">
71
-
<h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1>
72
-
$if(subtitle)$
73
-
<p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p>
74
-
$endif$
75
-
$for(author)$
76
-
<p class="text-sm text-gray-500 dark:text-gray-400">$author$</p>
77
-
$endfor$
78
-
$if(date)$
79
-
<p class="text-sm text-gray-500 dark:text-gray-400">$date$</p>
80
-
$endif$
81
-
$if(abstract)$
82
-
<div class="mt-6 p-4 bg-gray-50 rounded-lg">
83
-
<div class="text-sm font-semibold text-gray-700 uppercase mb-2">$abstract-title$</div>
84
-
<div class="text-gray-700">$abstract$</div>
85
-
</div>
86
-
$endif$
87
-
$endif$
88
-
</header>
108
+
<header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700">
109
+
<h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1>
110
+
$if(subtitle)$
111
+
<p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p>
112
+
$endif$
113
+
$for(author)$
114
+
<p class="text-sm text-gray-500 dark:text-gray-400">$author$</p>
115
+
$endfor$
116
+
$if(date)$
117
+
<p class="text-sm text-gray-500 dark:text-gray-400">Updated on $date$</p>
118
+
$endif$
119
+
$endif$
120
+
</header>
89
121
$endif$
122
+
123
+
$if(abstract)$
124
+
<article class="prose dark:prose-invert max-w-none">
125
+
$abstract$
126
+
</article>
127
+
$endif$
128
+
90
129
<article class="prose dark:prose-invert max-w-none">
91
130
$body$
92
131
</article>
93
132
</main>
94
-
<nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 ">
133
+
<nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
95
134
<div class="max-w-4xl mx-auto px-8 py-4">
96
135
<div class="flex justify-between gap-4">
97
136
<span class="flex-1">
+1
-1
flake.nix
+1
-1
flake.nix
···
76
76
};
77
77
buildGoApplication =
78
78
(self.callPackage "${gomod2nix}/builder" {
79
-
gomod2nix = gomod2nix.legacyPackages.${pkgs.system}.gomod2nix;
79
+
gomod2nix = gomod2nix.legacyPackages.${pkgs.stdenv.hostPlatform.system}.gomod2nix;
80
80
}).buildGoApplication;
81
81
modules = ./nix/gomod2nix.toml;
82
82
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
+2
-1
input.css
+2
-1
input.css
···
162
162
}
163
163
164
164
.prose a.mention {
165
-
@apply no-underline hover:underline;
165
+
@apply no-underline hover:underline font-bold;
166
166
}
167
167
168
168
.prose li {
···
255
255
@apply py-1 text-gray-900 dark:text-gray-100;
256
256
}
257
257
}
258
+
258
259
}
259
260
260
261
/* Background */
+10
-2
lexicons/pulls/pull.json
+10
-2
lexicons/pulls/pull.json
···
12
12
"required": [
13
13
"target",
14
14
"title",
15
-
"patch",
15
+
"patchBlob",
16
16
"createdAt"
17
17
],
18
18
"properties": {
···
27
27
"type": "string"
28
28
},
29
29
"patch": {
30
-
"type": "string"
30
+
"type": "string",
31
+
"description": "(deprecated) use patchBlob instead"
32
+
},
33
+
"patchBlob": {
34
+
"type": "blob",
35
+
"accept": [
36
+
"text/x-patch"
37
+
],
38
+
"description": "patch content"
31
39
},
32
40
"source": {
33
41
"type": "ref",
+13
-1
nix/pkgs/docs.nix
+13
-1
nix/pkgs/docs.nix
···
18
18
# icons
19
19
cp -rf ${lucide-src}/*.svg working/
20
20
21
-
# content
21
+
# content - chunked
22
22
${pandoc}/bin/pandoc ${src}/docs/DOCS.md \
23
23
-o $out/ \
24
24
-t chunkedhtml \
25
25
--variable toc \
26
+
--variable-json single-page=false \
26
27
--toc-depth=2 \
27
28
--css=stylesheet.css \
28
29
--chunk-template="%i.html" \
30
+
--highlight-style=working/highlight.theme \
31
+
--template=working/template.html
32
+
33
+
# content - single page
34
+
${pandoc}/bin/pandoc ${src}/docs/DOCS.md \
35
+
-o $out/single-page.html \
36
+
--toc \
37
+
--variable toc \
38
+
--variable single-page \
39
+
--toc-depth=2 \
40
+
--css=stylesheet.css \
29
41
--highlight-style=working/highlight.theme \
30
42
--template=working/template.html
31
43
+1
-1
nix/vm.nix
+1
-1
nix/vm.nix
···
8
8
var = builtins.getEnv name;
9
9
in
10
10
if var == ""
11
-
then throw "\$${name} must be defined, see docs/hacking.md for more details"
11
+
then throw "\$${name} must be defined, see https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled for more details"
12
12
else var;
13
13
envVarOr = name: default: let
14
14
var = builtins.getEnv name;
+3
-3
readme.md
+3
-3
readme.md
···
10
10
11
11
## docs
12
12
13
-
* [knot hosting guide](/docs/knot-hosting.md)
14
-
* [contributing guide](/docs/contributing.md) **please read before opening a PR!**
15
-
* [hacking on tangled](/docs/hacking.md)
13
+
- [knot hosting guide](https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide)
14
+
- [contributing guide](https://docs.tangled.org/contribution-guide.html#contribution-guide) **please read before opening a PR!**
15
+
- [hacking on tangled](https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled)
16
16
17
17
## security
18
18
+1
-1
spindle/motd
+1
-1
spindle/motd
+31
-13
spindle/server.go
+31
-13
spindle/server.go
···
8
8
"log/slog"
9
9
"maps"
10
10
"net/http"
11
+
"sync"
11
12
12
13
"github.com/go-chi/chi/v5"
13
14
"tangled.org/core/api/tangled"
···
30
31
)
31
32
32
33
//go:embed motd
33
-
var motd []byte
34
+
var defaultMotd []byte
34
35
35
36
const (
36
37
rbacDomain = "thisserver"
37
38
)
38
39
39
40
type Spindle struct {
40
-
jc *jetstream.JetstreamClient
41
-
db *db.DB
42
-
e *rbac.Enforcer
43
-
l *slog.Logger
44
-
n *notifier.Notifier
45
-
engs map[string]models.Engine
46
-
jq *queue.Queue
47
-
cfg *config.Config
48
-
ks *eventconsumer.Consumer
49
-
res *idresolver.Resolver
50
-
vault secrets.Manager
41
+
jc *jetstream.JetstreamClient
42
+
db *db.DB
43
+
e *rbac.Enforcer
44
+
l *slog.Logger
45
+
n *notifier.Notifier
46
+
engs map[string]models.Engine
47
+
jq *queue.Queue
48
+
cfg *config.Config
49
+
ks *eventconsumer.Consumer
50
+
res *idresolver.Resolver
51
+
vault secrets.Manager
52
+
motd []byte
53
+
motdMu sync.RWMutex
51
54
}
52
55
53
56
// New creates a new Spindle server with the provided configuration and engines.
···
128
131
cfg: cfg,
129
132
res: resolver,
130
133
vault: vault,
134
+
motd: defaultMotd,
131
135
}
132
136
133
137
err = e.AddSpindle(rbacDomain)
···
201
205
return s.e
202
206
}
203
207
208
+
// SetMotdContent sets custom MOTD content, replacing the embedded default.
209
+
func (s *Spindle) SetMotdContent(content []byte) {
210
+
s.motdMu.Lock()
211
+
defer s.motdMu.Unlock()
212
+
s.motd = content
213
+
}
214
+
215
+
// GetMotdContent returns the current MOTD content.
216
+
func (s *Spindle) GetMotdContent() []byte {
217
+
s.motdMu.RLock()
218
+
defer s.motdMu.RUnlock()
219
+
return s.motd
220
+
}
221
+
204
222
// Start starts the Spindle server (blocking).
205
223
func (s *Spindle) Start(ctx context.Context) error {
206
224
// starts a job queue runner in the background
···
246
264
mux := chi.NewRouter()
247
265
248
266
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
249
-
w.Write(motd)
267
+
w.Write(s.GetMotdContent())
250
268
})
251
269
mux.HandleFunc("/events", s.Events)
252
270
mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)
+3
types/diff.go
+3
types/diff.go
+112
types/diff_test.go
+112
types/diff_test.go
···
1
+
package types
2
+
3
+
import "testing"
4
+
5
+
func TestDiffId(t *testing.T) {
6
+
tests := []struct {
7
+
name string
8
+
diff Diff
9
+
expected string
10
+
}{
11
+
{
12
+
name: "regular file uses new name",
13
+
diff: Diff{
14
+
Name: struct {
15
+
Old string `json:"old"`
16
+
New string `json:"new"`
17
+
}{Old: "", New: "src/main.go"},
18
+
},
19
+
expected: "src/main.go",
20
+
},
21
+
{
22
+
name: "new file uses new name",
23
+
diff: Diff{
24
+
Name: struct {
25
+
Old string `json:"old"`
26
+
New string `json:"new"`
27
+
}{Old: "", New: "src/new.go"},
28
+
IsNew: true,
29
+
},
30
+
expected: "src/new.go",
31
+
},
32
+
{
33
+
name: "deleted file uses old name",
34
+
diff: Diff{
35
+
Name: struct {
36
+
Old string `json:"old"`
37
+
New string `json:"new"`
38
+
}{Old: "src/deleted.go", New: ""},
39
+
IsDelete: true,
40
+
},
41
+
expected: "src/deleted.go",
42
+
},
43
+
{
44
+
name: "renamed file uses new name",
45
+
diff: Diff{
46
+
Name: struct {
47
+
Old string `json:"old"`
48
+
New string `json:"new"`
49
+
}{Old: "src/old.go", New: "src/renamed.go"},
50
+
IsRename: true,
51
+
},
52
+
expected: "src/renamed.go",
53
+
},
54
+
}
55
+
56
+
for _, tt := range tests {
57
+
t.Run(tt.name, func(t *testing.T) {
58
+
if got := tt.diff.Id(); got != tt.expected {
59
+
t.Errorf("Diff.Id() = %q, want %q", got, tt.expected)
60
+
}
61
+
})
62
+
}
63
+
}
64
+
65
+
func TestChangedFilesMatchesDiffId(t *testing.T) {
66
+
// ChangedFiles() must return values matching each Diff's Id()
67
+
// so that sidebar links point to the correct anchors.
68
+
// Tests existing, deleted, new, and renamed files.
69
+
nd := NiceDiff{
70
+
Diff: []Diff{
71
+
{
72
+
Name: struct {
73
+
Old string `json:"old"`
74
+
New string `json:"new"`
75
+
}{Old: "", New: "src/modified.go"},
76
+
},
77
+
{
78
+
Name: struct {
79
+
Old string `json:"old"`
80
+
New string `json:"new"`
81
+
}{Old: "src/deleted.go", New: ""},
82
+
IsDelete: true,
83
+
},
84
+
{
85
+
Name: struct {
86
+
Old string `json:"old"`
87
+
New string `json:"new"`
88
+
}{Old: "", New: "src/new.go"},
89
+
IsNew: true,
90
+
},
91
+
{
92
+
Name: struct {
93
+
Old string `json:"old"`
94
+
New string `json:"new"`
95
+
}{Old: "src/old.go", New: "src/renamed.go"},
96
+
IsRename: true,
97
+
},
98
+
},
99
+
}
100
+
101
+
changedFiles := nd.ChangedFiles()
102
+
103
+
if len(changedFiles) != len(nd.Diff) {
104
+
t.Fatalf("ChangedFiles() returned %d items, want %d", len(changedFiles), len(nd.Diff))
105
+
}
106
+
107
+
for i, diff := range nd.Diff {
108
+
if changedFiles[i] != diff.Id() {
109
+
t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id())
110
+
}
111
+
}
112
+
}