+32
-32
appview/issues/issues.go
+32
-32
appview/issues/issues.go
···
81
82
func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
83
l := rp.logger.With("handler", "RepoSingleIssue")
84
-
user := rp.oauth.GetMultiAccountUser(r)
85
f, err := rp.repoResolver.Resolve(r)
86
if err != nil {
87
l.Error("failed to get repo and knot", "err", err)
···
102
103
userReactions := map[models.ReactionKind]bool{}
104
if user != nil {
105
-
userReactions = db.GetReactionStatusMap(rp.db, user.Active.Did, issue.AtUri())
106
}
107
108
backlinks, err := db.GetBacklinks(rp.db, issue.AtUri())
···
143
144
func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) {
145
l := rp.logger.With("handler", "EditIssue")
146
-
user := rp.oauth.GetMultiAccountUser(r)
147
148
issue, ok := r.Context().Value("issue").(*models.Issue)
149
if !ok {
···
182
return
183
}
184
185
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Active.Did, newIssue.Rkey)
186
if err != nil {
187
l.Error("failed to get record", "err", err)
188
rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.")
···
191
192
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
193
Collection: tangled.RepoIssueNSID,
194
-
Repo: user.Active.Did,
195
Rkey: newIssue.Rkey,
196
SwapRecord: ex.Cid,
197
Record: &lexutil.LexiconTypeDecoder{
···
292
293
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
294
l := rp.logger.With("handler", "CloseIssue")
295
-
user := rp.oauth.GetMultiAccountUser(r)
296
f, err := rp.repoResolver.Resolve(r)
297
if err != nil {
298
l.Error("failed to get repo and knot", "err", err)
···
306
return
307
}
308
309
-
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
310
isRepoOwner := roles.IsOwner()
311
isCollaborator := roles.IsCollaborator()
312
-
isIssueOwner := user.Active.Did == issue.Did
313
314
// TODO: make this more granular
315
if isIssueOwner || isRepoOwner || isCollaborator {
···
326
issue.Open = false
327
328
// notify about the issue closure
329
-
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue)
330
331
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
332
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
···
340
341
func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) {
342
l := rp.logger.With("handler", "ReopenIssue")
343
-
user := rp.oauth.GetMultiAccountUser(r)
344
f, err := rp.repoResolver.Resolve(r)
345
if err != nil {
346
l.Error("failed to get repo and knot", "err", err)
···
354
return
355
}
356
357
-
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
358
isRepoOwner := roles.IsOwner()
359
isCollaborator := roles.IsCollaborator()
360
-
isIssueOwner := user.Active.Did == issue.Did
361
362
if isCollaborator || isRepoOwner || isIssueOwner {
363
err := db.ReopenIssues(
···
373
issue.Open = true
374
375
// notify about the issue reopen
376
-
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue)
377
378
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
379
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
···
387
388
func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) {
389
l := rp.logger.With("handler", "NewIssueComment")
390
-
user := rp.oauth.GetMultiAccountUser(r)
391
f, err := rp.repoResolver.Resolve(r)
392
if err != nil {
393
l.Error("failed to get repo and knot", "err", err)
···
416
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417
418
comment := models.IssueComment{
419
-
Did: user.Active.Did,
420
Rkey: tid.TID(),
421
IssueAt: issue.AtUri().String(),
422
ReplyTo: replyTo,
···
495
496
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
497
l := rp.logger.With("handler", "IssueComment")
498
-
user := rp.oauth.GetMultiAccountUser(r)
499
500
issue, ok := r.Context().Value("issue").(*models.Issue)
501
if !ok {
···
531
532
func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
533
l := rp.logger.With("handler", "EditIssueComment")
534
-
user := rp.oauth.GetMultiAccountUser(r)
535
536
issue, ok := r.Context().Value("issue").(*models.Issue)
537
if !ok {
···
557
}
558
comment := comments[0]
559
560
-
if comment.Did != user.Active.Did {
561
-
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did)
562
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
563
return
564
}
···
608
// rkey is optional, it was introduced later
609
if newComment.Rkey != "" {
610
// update the record on pds
611
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey)
612
if err != nil {
613
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
614
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
617
618
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
619
Collection: tangled.RepoIssueCommentNSID,
620
-
Repo: user.Active.Did,
621
Rkey: newComment.Rkey,
622
SwapRecord: ex.Cid,
623
Record: &lexutil.LexiconTypeDecoder{
···
641
642
func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) {
643
l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder")
644
-
user := rp.oauth.GetMultiAccountUser(r)
645
646
issue, ok := r.Context().Value("issue").(*models.Issue)
647
if !ok {
···
677
678
func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) {
679
l := rp.logger.With("handler", "ReplyIssueComment")
680
-
user := rp.oauth.GetMultiAccountUser(r)
681
682
issue, ok := r.Context().Value("issue").(*models.Issue)
683
if !ok {
···
713
714
func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
715
l := rp.logger.With("handler", "DeleteIssueComment")
716
-
user := rp.oauth.GetMultiAccountUser(r)
717
718
issue, ok := r.Context().Value("issue").(*models.Issue)
719
if !ok {
···
739
}
740
comment := comments[0]
741
742
-
if comment.Did != user.Active.Did {
743
-
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did)
744
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
745
return
746
}
···
769
}
770
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
771
Collection: tangled.RepoIssueCommentNSID,
772
-
Repo: user.Active.Did,
773
Rkey: comment.Rkey,
774
})
775
if err != nil {
···
807
808
page := pagination.FromContext(r.Context())
809
810
-
user := rp.oauth.GetMultiAccountUser(r)
811
f, err := rp.repoResolver.Resolve(r)
812
if err != nil {
813
l.Error("failed to get repo and knot", "err", err)
···
884
}
885
886
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
887
-
LoggedInUser: rp.oauth.GetMultiAccountUser(r),
888
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
889
Issues: issues,
890
IssueCount: totalIssues,
···
897
898
func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) {
899
l := rp.logger.With("handler", "NewIssue")
900
-
user := rp.oauth.GetMultiAccountUser(r)
901
902
f, err := rp.repoResolver.Resolve(r)
903
if err != nil {
···
921
Title: r.FormValue("title"),
922
Body: body,
923
Open: true,
924
-
Did: user.Active.Did,
925
Created: time.Now(),
926
Mentions: mentions,
927
References: references,
···
945
}
946
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
947
Collection: tangled.RepoIssueNSID,
948
-
Repo: user.Active.Did,
949
Rkey: issue.Rkey,
950
Record: &lexutil.LexiconTypeDecoder{
951
Val: &record,
···
81
82
func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
83
l := rp.logger.With("handler", "RepoSingleIssue")
84
+
user := rp.oauth.GetUser(r)
85
f, err := rp.repoResolver.Resolve(r)
86
if err != nil {
87
l.Error("failed to get repo and knot", "err", err)
···
102
103
userReactions := map[models.ReactionKind]bool{}
104
if user != nil {
105
+
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
106
}
107
108
backlinks, err := db.GetBacklinks(rp.db, issue.AtUri())
···
143
144
func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) {
145
l := rp.logger.With("handler", "EditIssue")
146
+
user := rp.oauth.GetUser(r)
147
148
issue, ok := r.Context().Value("issue").(*models.Issue)
149
if !ok {
···
182
return
183
}
184
185
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Did, newIssue.Rkey)
186
if err != nil {
187
l.Error("failed to get record", "err", err)
188
rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.")
···
191
192
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
193
Collection: tangled.RepoIssueNSID,
194
+
Repo: user.Did,
195
Rkey: newIssue.Rkey,
196
SwapRecord: ex.Cid,
197
Record: &lexutil.LexiconTypeDecoder{
···
292
293
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
294
l := rp.logger.With("handler", "CloseIssue")
295
+
user := rp.oauth.GetUser(r)
296
f, err := rp.repoResolver.Resolve(r)
297
if err != nil {
298
l.Error("failed to get repo and knot", "err", err)
···
306
return
307
}
308
309
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
310
isRepoOwner := roles.IsOwner()
311
isCollaborator := roles.IsCollaborator()
312
+
isIssueOwner := user.Did == issue.Did
313
314
// TODO: make this more granular
315
if isIssueOwner || isRepoOwner || isCollaborator {
···
326
issue.Open = false
327
328
// notify about the issue closure
329
+
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
330
331
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
332
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
···
340
341
func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) {
342
l := rp.logger.With("handler", "ReopenIssue")
343
+
user := rp.oauth.GetUser(r)
344
f, err := rp.repoResolver.Resolve(r)
345
if err != nil {
346
l.Error("failed to get repo and knot", "err", err)
···
354
return
355
}
356
357
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
358
isRepoOwner := roles.IsOwner()
359
isCollaborator := roles.IsCollaborator()
360
+
isIssueOwner := user.Did == issue.Did
361
362
if isCollaborator || isRepoOwner || isIssueOwner {
363
err := db.ReopenIssues(
···
373
issue.Open = true
374
375
// notify about the issue reopen
376
+
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
377
378
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
379
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
···
387
388
func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) {
389
l := rp.logger.With("handler", "NewIssueComment")
390
+
user := rp.oauth.GetUser(r)
391
f, err := rp.repoResolver.Resolve(r)
392
if err != nil {
393
l.Error("failed to get repo and knot", "err", err)
···
416
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417
418
comment := models.IssueComment{
419
+
Did: user.Did,
420
Rkey: tid.TID(),
421
IssueAt: issue.AtUri().String(),
422
ReplyTo: replyTo,
···
495
496
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
497
l := rp.logger.With("handler", "IssueComment")
498
+
user := rp.oauth.GetUser(r)
499
500
issue, ok := r.Context().Value("issue").(*models.Issue)
501
if !ok {
···
531
532
func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
533
l := rp.logger.With("handler", "EditIssueComment")
534
+
user := rp.oauth.GetUser(r)
535
536
issue, ok := r.Context().Value("issue").(*models.Issue)
537
if !ok {
···
557
}
558
comment := comments[0]
559
560
+
if comment.Did != user.Did {
561
+
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
562
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
563
return
564
}
···
608
// rkey is optional, it was introduced later
609
if newComment.Rkey != "" {
610
// update the record on pds
611
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey)
612
if err != nil {
613
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
614
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
617
618
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
619
Collection: tangled.RepoIssueCommentNSID,
620
+
Repo: user.Did,
621
Rkey: newComment.Rkey,
622
SwapRecord: ex.Cid,
623
Record: &lexutil.LexiconTypeDecoder{
···
641
642
func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) {
643
l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder")
644
+
user := rp.oauth.GetUser(r)
645
646
issue, ok := r.Context().Value("issue").(*models.Issue)
647
if !ok {
···
677
678
func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) {
679
l := rp.logger.With("handler", "ReplyIssueComment")
680
+
user := rp.oauth.GetUser(r)
681
682
issue, ok := r.Context().Value("issue").(*models.Issue)
683
if !ok {
···
713
714
func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
715
l := rp.logger.With("handler", "DeleteIssueComment")
716
+
user := rp.oauth.GetUser(r)
717
718
issue, ok := r.Context().Value("issue").(*models.Issue)
719
if !ok {
···
739
}
740
comment := comments[0]
741
742
+
if comment.Did != user.Did {
743
+
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
744
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
745
return
746
}
···
769
}
770
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
771
Collection: tangled.RepoIssueCommentNSID,
772
+
Repo: user.Did,
773
Rkey: comment.Rkey,
774
})
775
if err != nil {
···
807
808
page := pagination.FromContext(r.Context())
809
810
+
user := rp.oauth.GetUser(r)
811
f, err := rp.repoResolver.Resolve(r)
812
if err != nil {
813
l.Error("failed to get repo and knot", "err", err)
···
884
}
885
886
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
887
+
LoggedInUser: rp.oauth.GetUser(r),
888
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
889
Issues: issues,
890
IssueCount: totalIssues,
···
897
898
func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) {
899
l := rp.logger.With("handler", "NewIssue")
900
+
user := rp.oauth.GetUser(r)
901
902
f, err := rp.repoResolver.Resolve(r)
903
if err != nil {
···
921
Title: r.FormValue("title"),
922
Body: body,
923
Open: true,
924
+
Did: user.Did,
925
Created: time.Now(),
926
Mentions: mentions,
927
References: references,
···
945
}
946
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
947
Collection: tangled.RepoIssueNSID,
948
+
Repo: user.Did,
949
Rkey: issue.Rkey,
950
Record: &lexutil.LexiconTypeDecoder{
951
Val: &record,
+31
-31
appview/knots/knots.go
+31
-31
appview/knots/knots.go
···
70
}
71
72
func (k *Knots) knots(w http.ResponseWriter, r *http.Request) {
73
-
user := k.OAuth.GetMultiAccountUser(r)
74
registrations, err := db.GetRegistrations(
75
k.Db,
76
-
orm.FilterEq("did", user.Active.Did),
77
)
78
if err != nil {
79
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
92
func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) {
93
l := k.Logger.With("handler", "dashboard")
94
95
-
user := k.OAuth.GetMultiAccountUser(r)
96
-
l = l.With("user", user.Active.Did)
97
98
domain := chi.URLParam(r, "domain")
99
if domain == "" {
···
103
104
registrations, err := db.GetRegistrations(
105
k.Db,
106
-
orm.FilterEq("did", user.Active.Did),
107
orm.FilterEq("domain", domain),
108
)
109
if err != nil {
···
154
}
155
156
func (k *Knots) register(w http.ResponseWriter, r *http.Request) {
157
-
user := k.OAuth.GetMultiAccountUser(r)
158
l := k.Logger.With("handler", "register")
159
160
noticeId := "register-error"
···
175
return
176
}
177
l = l.With("domain", domain)
178
-
l = l.With("user", user.Active.Did)
179
180
tx, err := k.Db.Begin()
181
if err != nil {
···
188
k.Enforcer.E.LoadPolicy()
189
}()
190
191
-
err = db.AddKnot(tx, domain, user.Active.Did)
192
if err != nil {
193
l.Error("failed to insert", "err", err)
194
fail()
···
210
return
211
}
212
213
-
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain)
214
var exCid *string
215
if ex != nil {
216
exCid = ex.Cid
···
219
// re-announce by registering under same rkey
220
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
221
Collection: tangled.KnotNSID,
222
-
Repo: user.Active.Did,
223
Rkey: domain,
224
Record: &lexutil.LexiconTypeDecoder{
225
Val: &tangled.Knot{
···
250
}
251
252
// begin verification
253
-
err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev)
254
if err != nil {
255
l.Error("verification failed", "err", err)
256
k.Pages.HxRefresh(w)
257
return
258
}
259
260
-
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did)
261
if err != nil {
262
l.Error("failed to mark verified", "err", err)
263
k.Pages.HxRefresh(w)
···
275
}
276
277
func (k *Knots) delete(w http.ResponseWriter, r *http.Request) {
278
-
user := k.OAuth.GetMultiAccountUser(r)
279
l := k.Logger.With("handler", "delete")
280
281
noticeId := "operation-error"
···
294
// get record from db first
295
registrations, err := db.GetRegistrations(
296
k.Db,
297
-
orm.FilterEq("did", user.Active.Did),
298
orm.FilterEq("domain", domain),
299
)
300
if err != nil {
···
322
323
err = db.DeleteKnot(
324
tx,
325
-
orm.FilterEq("did", user.Active.Did),
326
orm.FilterEq("domain", domain),
327
)
328
if err != nil {
···
350
351
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
352
Collection: tangled.KnotNSID,
353
-
Repo: user.Active.Did,
354
Rkey: domain,
355
})
356
if err != nil {
···
382
}
383
384
func (k *Knots) retry(w http.ResponseWriter, r *http.Request) {
385
-
user := k.OAuth.GetMultiAccountUser(r)
386
l := k.Logger.With("handler", "retry")
387
388
noticeId := "operation-error"
···
398
return
399
}
400
l = l.With("domain", domain)
401
-
l = l.With("user", user.Active.Did)
402
403
// get record from db first
404
registrations, err := db.GetRegistrations(
405
k.Db,
406
-
orm.FilterEq("did", user.Active.Did),
407
orm.FilterEq("domain", domain),
408
)
409
if err != nil {
···
419
registration := registrations[0]
420
421
// begin verification
422
-
err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev)
423
if err != nil {
424
l.Error("verification failed", "err", err)
425
···
437
return
438
}
439
440
-
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did)
441
if err != nil {
442
l.Error("failed to mark verified", "err", err)
443
k.Pages.Notice(w, noticeId, err.Error())
···
456
return
457
}
458
459
-
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain)
460
var exCid *string
461
if ex != nil {
462
exCid = ex.Cid
···
465
// ignore the error here
466
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
467
Collection: tangled.KnotNSID,
468
-
Repo: user.Active.Did,
469
Rkey: domain,
470
Record: &lexutil.LexiconTypeDecoder{
471
Val: &tangled.Knot{
···
494
// Get updated registration to show
495
registrations, err = db.GetRegistrations(
496
k.Db,
497
-
orm.FilterEq("did", user.Active.Did),
498
orm.FilterEq("domain", domain),
499
)
500
if err != nil {
···
516
}
517
518
func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) {
519
-
user := k.OAuth.GetMultiAccountUser(r)
520
l := k.Logger.With("handler", "addMember")
521
522
domain := chi.URLParam(r, "domain")
···
526
return
527
}
528
l = l.With("domain", domain)
529
-
l = l.With("user", user.Active.Did)
530
531
registrations, err := db.GetRegistrations(
532
k.Db,
533
-
orm.FilterEq("did", user.Active.Did),
534
orm.FilterEq("domain", domain),
535
orm.FilterIsNot("registered", "null"),
536
)
···
583
584
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
585
Collection: tangled.KnotMemberNSID,
586
-
Repo: user.Active.Did,
587
Rkey: rkey,
588
Record: &lexutil.LexiconTypeDecoder{
589
Val: &tangled.KnotMember{
···
618
}
619
620
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
621
-
user := k.OAuth.GetMultiAccountUser(r)
622
l := k.Logger.With("handler", "removeMember")
623
624
noticeId := "operation-error"
···
634
return
635
}
636
l = l.With("domain", domain)
637
-
l = l.With("user", user.Active.Did)
638
639
registrations, err := db.GetRegistrations(
640
k.Db,
641
-
orm.FilterEq("did", user.Active.Did),
642
orm.FilterEq("domain", domain),
643
orm.FilterIsNot("registered", "null"),
644
)
···
70
}
71
72
func (k *Knots) knots(w http.ResponseWriter, r *http.Request) {
73
+
user := k.OAuth.GetUser(r)
74
registrations, err := db.GetRegistrations(
75
k.Db,
76
+
orm.FilterEq("did", user.Did),
77
)
78
if err != nil {
79
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
92
func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) {
93
l := k.Logger.With("handler", "dashboard")
94
95
+
user := k.OAuth.GetUser(r)
96
+
l = l.With("user", user.Did)
97
98
domain := chi.URLParam(r, "domain")
99
if domain == "" {
···
103
104
registrations, err := db.GetRegistrations(
105
k.Db,
106
+
orm.FilterEq("did", user.Did),
107
orm.FilterEq("domain", domain),
108
)
109
if err != nil {
···
154
}
155
156
func (k *Knots) register(w http.ResponseWriter, r *http.Request) {
157
+
user := k.OAuth.GetUser(r)
158
l := k.Logger.With("handler", "register")
159
160
noticeId := "register-error"
···
175
return
176
}
177
l = l.With("domain", domain)
178
+
l = l.With("user", user.Did)
179
180
tx, err := k.Db.Begin()
181
if err != nil {
···
188
k.Enforcer.E.LoadPolicy()
189
}()
190
191
+
err = db.AddKnot(tx, domain, user.Did)
192
if err != nil {
193
l.Error("failed to insert", "err", err)
194
fail()
···
210
return
211
}
212
213
+
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain)
214
var exCid *string
215
if ex != nil {
216
exCid = ex.Cid
···
219
// re-announce by registering under same rkey
220
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
221
Collection: tangled.KnotNSID,
222
+
Repo: user.Did,
223
Rkey: domain,
224
Record: &lexutil.LexiconTypeDecoder{
225
Val: &tangled.Knot{
···
250
}
251
252
// begin verification
253
+
err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
254
if err != nil {
255
l.Error("verification failed", "err", err)
256
k.Pages.HxRefresh(w)
257
return
258
}
259
260
+
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
261
if err != nil {
262
l.Error("failed to mark verified", "err", err)
263
k.Pages.HxRefresh(w)
···
275
}
276
277
func (k *Knots) delete(w http.ResponseWriter, r *http.Request) {
278
+
user := k.OAuth.GetUser(r)
279
l := k.Logger.With("handler", "delete")
280
281
noticeId := "operation-error"
···
294
// get record from db first
295
registrations, err := db.GetRegistrations(
296
k.Db,
297
+
orm.FilterEq("did", user.Did),
298
orm.FilterEq("domain", domain),
299
)
300
if err != nil {
···
322
323
err = db.DeleteKnot(
324
tx,
325
+
orm.FilterEq("did", user.Did),
326
orm.FilterEq("domain", domain),
327
)
328
if err != nil {
···
350
351
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
352
Collection: tangled.KnotNSID,
353
+
Repo: user.Did,
354
Rkey: domain,
355
})
356
if err != nil {
···
382
}
383
384
func (k *Knots) retry(w http.ResponseWriter, r *http.Request) {
385
+
user := k.OAuth.GetUser(r)
386
l := k.Logger.With("handler", "retry")
387
388
noticeId := "operation-error"
···
398
return
399
}
400
l = l.With("domain", domain)
401
+
l = l.With("user", user.Did)
402
403
// get record from db first
404
registrations, err := db.GetRegistrations(
405
k.Db,
406
+
orm.FilterEq("did", user.Did),
407
orm.FilterEq("domain", domain),
408
)
409
if err != nil {
···
419
registration := registrations[0]
420
421
// begin verification
422
+
err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
423
if err != nil {
424
l.Error("verification failed", "err", err)
425
···
437
return
438
}
439
440
+
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
441
if err != nil {
442
l.Error("failed to mark verified", "err", err)
443
k.Pages.Notice(w, noticeId, err.Error())
···
456
return
457
}
458
459
+
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain)
460
var exCid *string
461
if ex != nil {
462
exCid = ex.Cid
···
465
// ignore the error here
466
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
467
Collection: tangled.KnotNSID,
468
+
Repo: user.Did,
469
Rkey: domain,
470
Record: &lexutil.LexiconTypeDecoder{
471
Val: &tangled.Knot{
···
494
// Get updated registration to show
495
registrations, err = db.GetRegistrations(
496
k.Db,
497
+
orm.FilterEq("did", user.Did),
498
orm.FilterEq("domain", domain),
499
)
500
if err != nil {
···
516
}
517
518
func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) {
519
+
user := k.OAuth.GetUser(r)
520
l := k.Logger.With("handler", "addMember")
521
522
domain := chi.URLParam(r, "domain")
···
526
return
527
}
528
l = l.With("domain", domain)
529
+
l = l.With("user", user.Did)
530
531
registrations, err := db.GetRegistrations(
532
k.Db,
533
+
orm.FilterEq("did", user.Did),
534
orm.FilterEq("domain", domain),
535
orm.FilterIsNot("registered", "null"),
536
)
···
583
584
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
585
Collection: tangled.KnotMemberNSID,
586
+
Repo: user.Did,
587
Rkey: rkey,
588
Record: &lexutil.LexiconTypeDecoder{
589
Val: &tangled.KnotMember{
···
618
}
619
620
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
621
+
user := k.OAuth.GetUser(r)
622
l := k.Logger.With("handler", "removeMember")
623
624
noticeId := "operation-error"
···
634
return
635
}
636
l = l.With("domain", domain)
637
+
l = l.With("user", user.Did)
638
639
registrations, err := db.GetRegistrations(
640
k.Db,
641
+
orm.FilterEq("did", user.Did),
642
orm.FilterEq("domain", domain),
643
orm.FilterIsNot("registered", "null"),
644
)
+2
-2
appview/labels/labels.go
+2
-2
appview/labels/labels.go
···
68
// - this handler should calculate the diff in order to create the labelop record
69
// - we need the diff in order to maintain a "history" of operations performed by users
70
func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) {
71
-
user := l.oauth.GetMultiAccountUser(r)
72
73
noticeId := "add-label-error"
74
···
82
return
83
}
84
85
-
did := user.Active.Did
86
rkey := tid.TID()
87
performedAt := time.Now()
88
indexedAt := time.Now()
···
68
// - this handler should calculate the diff in order to create the labelop record
69
// - we need the diff in order to maintain a "history" of operations performed by users
70
func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) {
71
+
user := l.oauth.GetUser(r)
72
73
noticeId := "add-label-error"
74
···
82
return
83
}
84
85
+
did := user.Did
86
rkey := tid.TID()
87
performedAt := time.Now()
88
indexedAt := time.Now()
+8
-6
appview/middleware/middleware.go
+8
-6
appview/middleware/middleware.go
···
115
return func(next http.Handler) http.Handler {
116
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117
// requires auth also
118
-
actor := mw.oauth.GetMultiAccountUser(r)
119
if actor == nil {
120
// we need a logged in user
121
log.Printf("not logged in, redirecting")
···
128
return
129
}
130
131
-
ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Active.Did, group, domain)
132
if err != nil || !ok {
133
-
log.Printf("%s does not have perms of a %s in domain %s", actor.Active.Did, group, domain)
134
http.Error(w, "Forbiden", http.StatusUnauthorized)
135
return
136
}
···
148
return func(next http.Handler) http.Handler {
149
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
150
// requires auth also
151
-
actor := mw.oauth.GetMultiAccountUser(r)
152
if actor == nil {
153
// we need a logged in user
154
log.Printf("not logged in, redirecting")
···
161
return
162
}
163
164
-
ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
165
if err != nil || !ok {
166
-
log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo())
167
http.Error(w, "Forbiden", http.StatusUnauthorized)
168
return
169
}
···
115
return func(next http.Handler) http.Handler {
116
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117
// requires auth also
118
+
actor := mw.oauth.GetUser(r)
119
if actor == nil {
120
// we need a logged in user
121
log.Printf("not logged in, redirecting")
···
128
return
129
}
130
131
+
ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
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)
135
http.Error(w, "Forbiden", http.StatusUnauthorized)
136
return
137
}
···
149
return func(next http.Handler) http.Handler {
150
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151
// requires auth also
152
+
actor := mw.oauth.GetUser(r)
153
if actor == nil {
154
// we need a logged in user
155
log.Printf("not logged in, redirecting")
···
162
return
163
}
164
165
+
ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
166
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())
169
http.Error(w, "Forbiden", http.StatusUnauthorized)
170
return
171
}
+6
-6
appview/notifications/notifications.go
+6
-6
appview/notifications/notifications.go
···
48
49
func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) {
50
l := n.logger.With("handler", "notificationsPage")
51
-
user := n.oauth.GetMultiAccountUser(r)
52
53
page := pagination.FromContext(r.Context())
54
55
total, err := db.CountNotifications(
56
n.db,
57
-
orm.FilterEq("recipient_did", user.Active.Did),
58
)
59
if err != nil {
60
l.Error("failed to get total notifications", "err", err)
···
65
notifications, err := db.GetNotificationsWithEntities(
66
n.db,
67
page,
68
-
orm.FilterEq("recipient_did", user.Active.Did),
69
)
70
if err != nil {
71
l.Error("failed to get notifications", "err", err)
···
73
return
74
}
75
76
-
err = db.MarkAllNotificationsRead(n.db, user.Active.Did)
77
if err != nil {
78
l.Error("failed to mark notifications as read", "err", err)
79
}
···
90
}
91
92
func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) {
93
-
user := n.oauth.GetMultiAccountUser(r)
94
if user == nil {
95
return
96
}
97
98
count, err := db.CountNotifications(
99
n.db,
100
-
orm.FilterEq("recipient_did", user.Active.Did),
101
orm.FilterEq("read", 0),
102
)
103
if err != nil {
···
48
49
func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) {
50
l := n.logger.With("handler", "notificationsPage")
51
+
user := n.oauth.GetUser(r)
52
53
page := pagination.FromContext(r.Context())
54
55
total, err := db.CountNotifications(
56
n.db,
57
+
orm.FilterEq("recipient_did", user.Did),
58
)
59
if err != nil {
60
l.Error("failed to get total notifications", "err", err)
···
65
notifications, err := db.GetNotificationsWithEntities(
66
n.db,
67
page,
68
+
orm.FilterEq("recipient_did", user.Did),
69
)
70
if err != nil {
71
l.Error("failed to get notifications", "err", err)
···
73
return
74
}
75
76
+
err = db.MarkAllNotificationsRead(n.db, user.Did)
77
if err != nil {
78
l.Error("failed to mark notifications as read", "err", err)
79
}
···
90
}
91
92
func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) {
93
+
user := n.oauth.GetUser(r)
94
if user == nil {
95
return
96
}
97
98
count, err := db.CountNotifications(
99
n.db,
100
+
orm.FilterEq("recipient_did", user.Did),
101
orm.FilterEq("read", 0),
102
)
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
-
}
···
+1
-5
appview/oauth/consts.go
+1
-5
appview/oauth/consts.go
+2
-14
appview/oauth/handler.go
+2
-14
appview/oauth/handler.go
···
55
ctx := r.Context()
56
l := o.Logger.With("query", r.URL.Query())
57
58
-
authReturn := o.GetAuthReturn(r)
59
-
_ = o.ClearAuthReturn(w, r)
60
-
61
sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query())
62
if err != nil {
63
var callbackErr *oauth.AuthRequestCallbackError
···
73
74
if err := o.SaveSession(w, r, sessData); err != nil {
75
l.Error("failed to save session", "data", sessData, "err", err)
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)
81
return
82
}
83
···
95
}
96
}
97
98
-
redirectURL := "/"
99
-
if authReturn.ReturnURL != "" {
100
-
redirectURL = authReturn.ReturnURL
101
-
}
102
-
103
-
http.Redirect(w, r, redirectURL, http.StatusFound)
104
}
105
106
func (o *OAuth) addToDefaultSpindle(did string) {
···
55
ctx := r.Context()
56
l := o.Logger.With("query", r.URL.Query())
57
58
sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query())
59
if err != nil {
60
var callbackErr *oauth.AuthRequestCallbackError
···
70
71
if err := o.SaveSession(w, r, sessData); err != nil {
72
l.Error("failed to save session", "data", sessData, "err", err)
73
+
http.Redirect(w, r, "/login?error=session", http.StatusFound)
74
return
75
}
76
···
88
}
89
}
90
91
+
http.Redirect(w, r, "/", http.StatusFound)
92
}
93
94
func (o *OAuth) addToDefaultSpindle(did string) {
+4
-66
appview/oauth/oauth.go
+4
-66
appview/oauth/oauth.go
···
98
}
99
100
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error {
101
userSession, err := o.SessStore.Get(r, SessionName)
102
if err != nil {
103
return err
···
107
userSession.Values[SessionPds] = sessData.HostURL
108
userSession.Values[SessionId] = sessData.SessionID
109
userSession.Values[SessionAuthenticated] = true
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)
126
}
127
128
func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) {
···
177
return errors.Join(err1, err2)
178
}
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
-
228
type User struct {
229
Did string
230
Pds string
···
243
}
244
245
func (o *OAuth) GetDid(r *http.Request) string {
246
-
if u := o.GetMultiAccountUser(r); u != nil {
247
-
return u.Did()
248
}
249
250
return ""
···
98
}
99
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
userSession, err := o.SessStore.Get(r, SessionName)
103
if err != nil {
104
return err
···
108
userSession.Values[SessionPds] = sessData.HostURL
109
userSession.Values[SessionId] = sessData.SessionID
110
userSession.Values[SessionAuthenticated] = true
111
+
return userSession.Save(r, w)
112
}
113
114
func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) {
···
163
return errors.Join(err1, err2)
164
}
165
166
type User struct {
167
Did string
168
Pds string
···
181
}
182
183
func (o *OAuth) GetDid(r *http.Request) string {
184
+
if u := o.GetUser(r); u != nil {
185
+
return u.Did
186
}
187
188
return ""
+4
-4
appview/ogcard/card.go
+4
-4
appview/ogcard/card.go
···
453
454
// Handle SVG separately
455
if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") {
456
-
return c.convertSVGToPNG(bodyBytes)
457
}
458
459
// Support content types are in-sync with the allowed custom avatar file types
···
493
}
494
495
// convertSVGToPNG converts SVG data to a PNG image
496
-
func (c *Card) convertSVGToPNG(svgData []byte) (image.Image, bool) {
497
// Parse the SVG
498
icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData))
499
if err != nil {
···
547
draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil)
548
549
// Draw the image with circular clipping
550
-
for cy := 0; cy < size; cy++ {
551
-
for cx := 0; cx < size; cx++ {
552
// Calculate distance from center
553
dx := float64(cx - center)
554
dy := float64(cy - center)
···
453
454
// Handle SVG separately
455
if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") {
456
+
return convertSVGToPNG(bodyBytes)
457
}
458
459
// Support content types are in-sync with the allowed custom avatar file types
···
493
}
494
495
// convertSVGToPNG converts SVG data to a PNG image
496
+
func convertSVGToPNG(svgData []byte) (image.Image, bool) {
497
// Parse the SVG
498
icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData))
499
if err != nil {
···
547
draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil)
548
549
// Draw the image with circular clipping
550
+
for cy := range size {
551
+
for cx := range size {
552
// Calculate distance from center
553
dx := float64(cx - center)
554
dy := float64(cy - center)
-10
appview/pages/funcmap.go
-10
appview/pages/funcmap.go
···
28
emoji "github.com/yuin/goldmark-emoji"
29
"tangled.org/core/appview/filetree"
30
"tangled.org/core/appview/models"
31
-
"tangled.org/core/appview/oauth"
32
"tangled.org/core/appview/pages/markup"
33
"tangled.org/core/crypto"
34
)
···
385
return "error"
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
397
},
398
}
399
}
+66
-68
appview/pages/pages.go
+66
-68
appview/pages/pages.go
···
215
}
216
217
type LoginParams struct {
218
-
ReturnUrl string
219
-
ErrorCode string
220
-
AddAccount bool
221
-
LoggedInUser *oauth.MultiAccountUser
222
}
223
224
func (p *Pages) Login(w io.Writer, params LoginParams) error {
···
238
}
239
240
type TermsOfServiceParams struct {
241
-
LoggedInUser *oauth.MultiAccountUser
242
Content template.HTML
243
}
244
···
266
}
267
268
type PrivacyPolicyParams struct {
269
-
LoggedInUser *oauth.MultiAccountUser
270
Content template.HTML
271
}
272
···
294
}
295
296
type BrandParams struct {
297
-
LoggedInUser *oauth.MultiAccountUser
298
}
299
300
func (p *Pages) Brand(w io.Writer, params BrandParams) error {
···
302
}
303
304
type TimelineParams struct {
305
-
LoggedInUser *oauth.MultiAccountUser
306
Timeline []models.TimelineEvent
307
Repos []models.Repo
308
GfiLabel *models.LabelDefinition
···
313
}
314
315
type GoodFirstIssuesParams struct {
316
-
LoggedInUser *oauth.MultiAccountUser
317
Issues []models.Issue
318
RepoGroups []*models.RepoGroup
319
LabelDefs map[string]*models.LabelDefinition
···
326
}
327
328
type UserProfileSettingsParams struct {
329
-
LoggedInUser *oauth.MultiAccountUser
330
Tabs []map[string]any
331
Tab string
332
}
···
336
}
337
338
type NotificationsParams struct {
339
-
LoggedInUser *oauth.MultiAccountUser
340
Notifications []*models.NotificationWithEntity
341
UnreadCount int
342
Page pagination.Page
···
364
}
365
366
type UserKeysSettingsParams struct {
367
-
LoggedInUser *oauth.MultiAccountUser
368
PubKeys []models.PublicKey
369
Tabs []map[string]any
370
Tab string
···
375
}
376
377
type UserEmailsSettingsParams struct {
378
-
LoggedInUser *oauth.MultiAccountUser
379
Emails []models.Email
380
Tabs []map[string]any
381
Tab string
···
386
}
387
388
type UserNotificationSettingsParams struct {
389
-
LoggedInUser *oauth.MultiAccountUser
390
Preferences *models.NotificationPreferences
391
Tabs []map[string]any
392
Tab string
···
406
}
407
408
type KnotsParams struct {
409
-
LoggedInUser *oauth.MultiAccountUser
410
Registrations []models.Registration
411
Tabs []map[string]any
412
Tab string
···
417
}
418
419
type KnotParams struct {
420
-
LoggedInUser *oauth.MultiAccountUser
421
Registration *models.Registration
422
Members []string
423
Repos map[string][]models.Repo
···
439
}
440
441
type SpindlesParams struct {
442
-
LoggedInUser *oauth.MultiAccountUser
443
Spindles []models.Spindle
444
Tabs []map[string]any
445
Tab string
···
460
}
461
462
type SpindleDashboardParams struct {
463
-
LoggedInUser *oauth.MultiAccountUser
464
Spindle models.Spindle
465
Members []string
466
Repos map[string][]models.Repo
···
473
}
474
475
type NewRepoParams struct {
476
-
LoggedInUser *oauth.MultiAccountUser
477
Knots []string
478
}
479
···
482
}
483
484
type ForkRepoParams struct {
485
-
LoggedInUser *oauth.MultiAccountUser
486
Knots []string
487
RepoInfo repoinfo.RepoInfo
488
}
···
520
}
521
522
type ProfileOverviewParams struct {
523
-
LoggedInUser *oauth.MultiAccountUser
524
Repos []models.Repo
525
CollaboratingRepos []models.Repo
526
ProfileTimeline *models.ProfileTimeline
···
534
}
535
536
type ProfileReposParams struct {
537
-
LoggedInUser *oauth.MultiAccountUser
538
Repos []models.Repo
539
Card *ProfileCard
540
Active string
···
546
}
547
548
type ProfileStarredParams struct {
549
-
LoggedInUser *oauth.MultiAccountUser
550
Repos []models.Repo
551
Card *ProfileCard
552
Active string
···
558
}
559
560
type ProfileStringsParams struct {
561
-
LoggedInUser *oauth.MultiAccountUser
562
Strings []models.String
563
Card *ProfileCard
564
Active string
···
571
572
type FollowCard struct {
573
UserDid string
574
-
LoggedInUser *oauth.MultiAccountUser
575
FollowStatus models.FollowStatus
576
FollowersCount int64
577
FollowingCount int64
···
579
}
580
581
type ProfileFollowersParams struct {
582
-
LoggedInUser *oauth.MultiAccountUser
583
Followers []FollowCard
584
Card *ProfileCard
585
Active string
···
591
}
592
593
type ProfileFollowingParams struct {
594
-
LoggedInUser *oauth.MultiAccountUser
595
Following []FollowCard
596
Card *ProfileCard
597
Active string
···
612
}
613
614
type EditBioParams struct {
615
-
LoggedInUser *oauth.MultiAccountUser
616
Profile *models.Profile
617
}
618
···
621
}
622
623
type EditPinsParams struct {
624
-
LoggedInUser *oauth.MultiAccountUser
625
Profile *models.Profile
626
AllRepos []PinnedRepo
627
}
···
646
}
647
648
type RepoIndexParams struct {
649
-
LoggedInUser *oauth.MultiAccountUser
650
RepoInfo repoinfo.RepoInfo
651
Active string
652
TagMap map[string][]string
···
695
}
696
697
type RepoLogParams struct {
698
-
LoggedInUser *oauth.MultiAccountUser
699
RepoInfo repoinfo.RepoInfo
700
TagMap map[string][]string
701
Active string
···
712
}
713
714
type RepoCommitParams struct {
715
-
LoggedInUser *oauth.MultiAccountUser
716
RepoInfo repoinfo.RepoInfo
717
Active string
718
EmailToDid map[string]string
···
731
}
732
733
type RepoTreeParams struct {
734
-
LoggedInUser *oauth.MultiAccountUser
735
RepoInfo repoinfo.RepoInfo
736
Active string
737
BreadCrumbs [][]string
···
786
}
787
788
type RepoBranchesParams struct {
789
-
LoggedInUser *oauth.MultiAccountUser
790
RepoInfo repoinfo.RepoInfo
791
Active string
792
types.RepoBranchesResponse
···
798
}
799
800
type RepoTagsParams struct {
801
-
LoggedInUser *oauth.MultiAccountUser
802
RepoInfo repoinfo.RepoInfo
803
Active string
804
types.RepoTagsResponse
···
812
}
813
814
type RepoArtifactParams struct {
815
-
LoggedInUser *oauth.MultiAccountUser
816
RepoInfo repoinfo.RepoInfo
817
Artifact models.Artifact
818
}
···
822
}
823
824
type RepoBlobParams struct {
825
-
LoggedInUser *oauth.MultiAccountUser
826
RepoInfo repoinfo.RepoInfo
827
Active string
828
BreadCrumbs [][]string
···
846
}
847
848
type RepoSettingsParams struct {
849
-
LoggedInUser *oauth.MultiAccountUser
850
RepoInfo repoinfo.RepoInfo
851
Collaborators []Collaborator
852
Active string
···
865
}
866
867
type RepoGeneralSettingsParams struct {
868
-
LoggedInUser *oauth.MultiAccountUser
869
RepoInfo repoinfo.RepoInfo
870
Labels []models.LabelDefinition
871
DefaultLabels []models.LabelDefinition
···
883
}
884
885
type RepoAccessSettingsParams struct {
886
-
LoggedInUser *oauth.MultiAccountUser
887
RepoInfo repoinfo.RepoInfo
888
Active string
889
Tabs []map[string]any
···
897
}
898
899
type RepoPipelineSettingsParams struct {
900
-
LoggedInUser *oauth.MultiAccountUser
901
RepoInfo repoinfo.RepoInfo
902
Active string
903
Tabs []map[string]any
···
913
}
914
915
type RepoIssuesParams struct {
916
-
LoggedInUser *oauth.MultiAccountUser
917
RepoInfo repoinfo.RepoInfo
918
Active string
919
Issues []models.Issue
···
930
}
931
932
type RepoSingleIssueParams struct {
933
-
LoggedInUser *oauth.MultiAccountUser
934
RepoInfo repoinfo.RepoInfo
935
Active string
936
Issue *models.Issue
···
949
}
950
951
type EditIssueParams struct {
952
-
LoggedInUser *oauth.MultiAccountUser
953
RepoInfo repoinfo.RepoInfo
954
Issue *models.Issue
955
Action string
···
973
}
974
975
type RepoNewIssueParams struct {
976
-
LoggedInUser *oauth.MultiAccountUser
977
RepoInfo repoinfo.RepoInfo
978
Issue *models.Issue // existing issue if any -- passed when editing
979
Active string
···
987
}
988
989
type EditIssueCommentParams struct {
990
-
LoggedInUser *oauth.MultiAccountUser
991
RepoInfo repoinfo.RepoInfo
992
Issue *models.Issue
993
Comment *models.IssueComment
···
998
}
999
1000
type ReplyIssueCommentPlaceholderParams struct {
1001
-
LoggedInUser *oauth.MultiAccountUser
1002
RepoInfo repoinfo.RepoInfo
1003
Issue *models.Issue
1004
Comment *models.IssueComment
···
1009
}
1010
1011
type ReplyIssueCommentParams struct {
1012
-
LoggedInUser *oauth.MultiAccountUser
1013
RepoInfo repoinfo.RepoInfo
1014
Issue *models.Issue
1015
Comment *models.IssueComment
···
1020
}
1021
1022
type IssueCommentBodyParams struct {
1023
-
LoggedInUser *oauth.MultiAccountUser
1024
RepoInfo repoinfo.RepoInfo
1025
Issue *models.Issue
1026
Comment *models.IssueComment
···
1031
}
1032
1033
type RepoNewPullParams struct {
1034
-
LoggedInUser *oauth.MultiAccountUser
1035
RepoInfo repoinfo.RepoInfo
1036
Branches []types.Branch
1037
Strategy string
···
1048
}
1049
1050
type RepoPullsParams struct {
1051
-
LoggedInUser *oauth.MultiAccountUser
1052
RepoInfo repoinfo.RepoInfo
1053
Pulls []*models.Pull
1054
Active string
···
1083
}
1084
1085
type RepoSinglePullParams struct {
1086
-
LoggedInUser *oauth.MultiAccountUser
1087
RepoInfo repoinfo.RepoInfo
1088
Active string
1089
Pull *models.Pull
···
1108
}
1109
1110
type RepoPullPatchParams struct {
1111
-
LoggedInUser *oauth.MultiAccountUser
1112
RepoInfo repoinfo.RepoInfo
1113
Pull *models.Pull
1114
Stack models.Stack
···
1125
}
1126
1127
type RepoPullInterdiffParams struct {
1128
-
LoggedInUser *oauth.MultiAccountUser
1129
RepoInfo repoinfo.RepoInfo
1130
Pull *models.Pull
1131
Round int
···
1178
}
1179
1180
type PullResubmitParams struct {
1181
-
LoggedInUser *oauth.MultiAccountUser
1182
RepoInfo repoinfo.RepoInfo
1183
Pull *models.Pull
1184
SubmissionId int
···
1189
}
1190
1191
type PullActionsParams struct {
1192
-
LoggedInUser *oauth.MultiAccountUser
1193
RepoInfo repoinfo.RepoInfo
1194
Pull *models.Pull
1195
RoundNumber int
···
1204
}
1205
1206
type PullNewCommentParams struct {
1207
-
LoggedInUser *oauth.MultiAccountUser
1208
RepoInfo repoinfo.RepoInfo
1209
Pull *models.Pull
1210
RoundNumber int
···
1215
}
1216
1217
type RepoCompareParams struct {
1218
-
LoggedInUser *oauth.MultiAccountUser
1219
RepoInfo repoinfo.RepoInfo
1220
Forks []models.Repo
1221
Branches []types.Branch
···
1234
}
1235
1236
type RepoCompareNewParams struct {
1237
-
LoggedInUser *oauth.MultiAccountUser
1238
RepoInfo repoinfo.RepoInfo
1239
Forks []models.Repo
1240
Branches []types.Branch
···
1251
}
1252
1253
type RepoCompareAllowPullParams struct {
1254
-
LoggedInUser *oauth.MultiAccountUser
1255
RepoInfo repoinfo.RepoInfo
1256
Base string
1257
Head string
···
1271
}
1272
1273
type LabelPanelParams struct {
1274
-
LoggedInUser *oauth.MultiAccountUser
1275
RepoInfo repoinfo.RepoInfo
1276
Defs map[string]*models.LabelDefinition
1277
Subject string
···
1283
}
1284
1285
type EditLabelPanelParams struct {
1286
-
LoggedInUser *oauth.MultiAccountUser
1287
RepoInfo repoinfo.RepoInfo
1288
Defs map[string]*models.LabelDefinition
1289
Subject string
···
1295
}
1296
1297
type PipelinesParams struct {
1298
-
LoggedInUser *oauth.MultiAccountUser
1299
RepoInfo repoinfo.RepoInfo
1300
Pipelines []models.Pipeline
1301
Active string
···
1338
}
1339
1340
type WorkflowParams struct {
1341
-
LoggedInUser *oauth.MultiAccountUser
1342
RepoInfo repoinfo.RepoInfo
1343
Pipeline models.Pipeline
1344
Workflow string
···
1352
}
1353
1354
type PutStringParams struct {
1355
-
LoggedInUser *oauth.MultiAccountUser
1356
Action string
1357
1358
// this is supplied in the case of editing an existing string
···
1364
}
1365
1366
type StringsDashboardParams struct {
1367
-
LoggedInUser *oauth.MultiAccountUser
1368
Card ProfileCard
1369
Strings []models.String
1370
}
···
1374
}
1375
1376
type StringTimelineParams struct {
1377
-
LoggedInUser *oauth.MultiAccountUser
1378
Strings []models.String
1379
}
1380
···
1383
}
1384
1385
type SingleStringParams struct {
1386
-
LoggedInUser *oauth.MultiAccountUser
1387
ShowRendered bool
1388
RenderToggle bool
1389
RenderedContents template.HTML
···
215
}
216
217
type LoginParams struct {
218
+
ReturnUrl string
219
+
ErrorCode string
220
}
221
222
func (p *Pages) Login(w io.Writer, params LoginParams) error {
···
236
}
237
238
type TermsOfServiceParams struct {
239
+
LoggedInUser *oauth.User
240
Content template.HTML
241
}
242
···
264
}
265
266
type PrivacyPolicyParams struct {
267
+
LoggedInUser *oauth.User
268
Content template.HTML
269
}
270
···
292
}
293
294
type BrandParams struct {
295
+
LoggedInUser *oauth.User
296
}
297
298
func (p *Pages) Brand(w io.Writer, params BrandParams) error {
···
300
}
301
302
type TimelineParams struct {
303
+
LoggedInUser *oauth.User
304
Timeline []models.TimelineEvent
305
Repos []models.Repo
306
GfiLabel *models.LabelDefinition
···
311
}
312
313
type GoodFirstIssuesParams struct {
314
+
LoggedInUser *oauth.User
315
Issues []models.Issue
316
RepoGroups []*models.RepoGroup
317
LabelDefs map[string]*models.LabelDefinition
···
324
}
325
326
type UserProfileSettingsParams struct {
327
+
LoggedInUser *oauth.User
328
Tabs []map[string]any
329
Tab string
330
}
···
334
}
335
336
type NotificationsParams struct {
337
+
LoggedInUser *oauth.User
338
Notifications []*models.NotificationWithEntity
339
UnreadCount int
340
Page pagination.Page
···
362
}
363
364
type UserKeysSettingsParams struct {
365
+
LoggedInUser *oauth.User
366
PubKeys []models.PublicKey
367
Tabs []map[string]any
368
Tab string
···
373
}
374
375
type UserEmailsSettingsParams struct {
376
+
LoggedInUser *oauth.User
377
Emails []models.Email
378
Tabs []map[string]any
379
Tab string
···
384
}
385
386
type UserNotificationSettingsParams struct {
387
+
LoggedInUser *oauth.User
388
Preferences *models.NotificationPreferences
389
Tabs []map[string]any
390
Tab string
···
404
}
405
406
type KnotsParams struct {
407
+
LoggedInUser *oauth.User
408
Registrations []models.Registration
409
Tabs []map[string]any
410
Tab string
···
415
}
416
417
type KnotParams struct {
418
+
LoggedInUser *oauth.User
419
Registration *models.Registration
420
Members []string
421
Repos map[string][]models.Repo
···
437
}
438
439
type SpindlesParams struct {
440
+
LoggedInUser *oauth.User
441
Spindles []models.Spindle
442
Tabs []map[string]any
443
Tab string
···
458
}
459
460
type SpindleDashboardParams struct {
461
+
LoggedInUser *oauth.User
462
Spindle models.Spindle
463
Members []string
464
Repos map[string][]models.Repo
···
471
}
472
473
type NewRepoParams struct {
474
+
LoggedInUser *oauth.User
475
Knots []string
476
}
477
···
480
}
481
482
type ForkRepoParams struct {
483
+
LoggedInUser *oauth.User
484
Knots []string
485
RepoInfo repoinfo.RepoInfo
486
}
···
518
}
519
520
type ProfileOverviewParams struct {
521
+
LoggedInUser *oauth.User
522
Repos []models.Repo
523
CollaboratingRepos []models.Repo
524
ProfileTimeline *models.ProfileTimeline
···
532
}
533
534
type ProfileReposParams struct {
535
+
LoggedInUser *oauth.User
536
Repos []models.Repo
537
Card *ProfileCard
538
Active string
···
544
}
545
546
type ProfileStarredParams struct {
547
+
LoggedInUser *oauth.User
548
Repos []models.Repo
549
Card *ProfileCard
550
Active string
···
556
}
557
558
type ProfileStringsParams struct {
559
+
LoggedInUser *oauth.User
560
Strings []models.String
561
Card *ProfileCard
562
Active string
···
569
570
type FollowCard struct {
571
UserDid string
572
+
LoggedInUser *oauth.User
573
FollowStatus models.FollowStatus
574
FollowersCount int64
575
FollowingCount int64
···
577
}
578
579
type ProfileFollowersParams struct {
580
+
LoggedInUser *oauth.User
581
Followers []FollowCard
582
Card *ProfileCard
583
Active string
···
589
}
590
591
type ProfileFollowingParams struct {
592
+
LoggedInUser *oauth.User
593
Following []FollowCard
594
Card *ProfileCard
595
Active string
···
610
}
611
612
type EditBioParams struct {
613
+
LoggedInUser *oauth.User
614
Profile *models.Profile
615
}
616
···
619
}
620
621
type EditPinsParams struct {
622
+
LoggedInUser *oauth.User
623
Profile *models.Profile
624
AllRepos []PinnedRepo
625
}
···
644
}
645
646
type RepoIndexParams struct {
647
+
LoggedInUser *oauth.User
648
RepoInfo repoinfo.RepoInfo
649
Active string
650
TagMap map[string][]string
···
693
}
694
695
type RepoLogParams struct {
696
+
LoggedInUser *oauth.User
697
RepoInfo repoinfo.RepoInfo
698
TagMap map[string][]string
699
Active string
···
710
}
711
712
type RepoCommitParams struct {
713
+
LoggedInUser *oauth.User
714
RepoInfo repoinfo.RepoInfo
715
Active string
716
EmailToDid map[string]string
···
729
}
730
731
type RepoTreeParams struct {
732
+
LoggedInUser *oauth.User
733
RepoInfo repoinfo.RepoInfo
734
Active string
735
BreadCrumbs [][]string
···
784
}
785
786
type RepoBranchesParams struct {
787
+
LoggedInUser *oauth.User
788
RepoInfo repoinfo.RepoInfo
789
Active string
790
types.RepoBranchesResponse
···
796
}
797
798
type RepoTagsParams struct {
799
+
LoggedInUser *oauth.User
800
RepoInfo repoinfo.RepoInfo
801
Active string
802
types.RepoTagsResponse
···
810
}
811
812
type RepoArtifactParams struct {
813
+
LoggedInUser *oauth.User
814
RepoInfo repoinfo.RepoInfo
815
Artifact models.Artifact
816
}
···
820
}
821
822
type RepoBlobParams struct {
823
+
LoggedInUser *oauth.User
824
RepoInfo repoinfo.RepoInfo
825
Active string
826
BreadCrumbs [][]string
···
844
}
845
846
type RepoSettingsParams struct {
847
+
LoggedInUser *oauth.User
848
RepoInfo repoinfo.RepoInfo
849
Collaborators []Collaborator
850
Active string
···
863
}
864
865
type RepoGeneralSettingsParams struct {
866
+
LoggedInUser *oauth.User
867
RepoInfo repoinfo.RepoInfo
868
Labels []models.LabelDefinition
869
DefaultLabels []models.LabelDefinition
···
881
}
882
883
type RepoAccessSettingsParams struct {
884
+
LoggedInUser *oauth.User
885
RepoInfo repoinfo.RepoInfo
886
Active string
887
Tabs []map[string]any
···
895
}
896
897
type RepoPipelineSettingsParams struct {
898
+
LoggedInUser *oauth.User
899
RepoInfo repoinfo.RepoInfo
900
Active string
901
Tabs []map[string]any
···
911
}
912
913
type RepoIssuesParams struct {
914
+
LoggedInUser *oauth.User
915
RepoInfo repoinfo.RepoInfo
916
Active string
917
Issues []models.Issue
···
928
}
929
930
type RepoSingleIssueParams struct {
931
+
LoggedInUser *oauth.User
932
RepoInfo repoinfo.RepoInfo
933
Active string
934
Issue *models.Issue
···
947
}
948
949
type EditIssueParams struct {
950
+
LoggedInUser *oauth.User
951
RepoInfo repoinfo.RepoInfo
952
Issue *models.Issue
953
Action string
···
971
}
972
973
type RepoNewIssueParams struct {
974
+
LoggedInUser *oauth.User
975
RepoInfo repoinfo.RepoInfo
976
Issue *models.Issue // existing issue if any -- passed when editing
977
Active string
···
985
}
986
987
type EditIssueCommentParams struct {
988
+
LoggedInUser *oauth.User
989
RepoInfo repoinfo.RepoInfo
990
Issue *models.Issue
991
Comment *models.IssueComment
···
996
}
997
998
type ReplyIssueCommentPlaceholderParams struct {
999
+
LoggedInUser *oauth.User
1000
RepoInfo repoinfo.RepoInfo
1001
Issue *models.Issue
1002
Comment *models.IssueComment
···
1007
}
1008
1009
type ReplyIssueCommentParams struct {
1010
+
LoggedInUser *oauth.User
1011
RepoInfo repoinfo.RepoInfo
1012
Issue *models.Issue
1013
Comment *models.IssueComment
···
1018
}
1019
1020
type IssueCommentBodyParams struct {
1021
+
LoggedInUser *oauth.User
1022
RepoInfo repoinfo.RepoInfo
1023
Issue *models.Issue
1024
Comment *models.IssueComment
···
1029
}
1030
1031
type RepoNewPullParams struct {
1032
+
LoggedInUser *oauth.User
1033
RepoInfo repoinfo.RepoInfo
1034
Branches []types.Branch
1035
Strategy string
···
1046
}
1047
1048
type RepoPullsParams struct {
1049
+
LoggedInUser *oauth.User
1050
RepoInfo repoinfo.RepoInfo
1051
Pulls []*models.Pull
1052
Active string
···
1081
}
1082
1083
type RepoSinglePullParams struct {
1084
+
LoggedInUser *oauth.User
1085
RepoInfo repoinfo.RepoInfo
1086
Active string
1087
Pull *models.Pull
···
1106
}
1107
1108
type RepoPullPatchParams struct {
1109
+
LoggedInUser *oauth.User
1110
RepoInfo repoinfo.RepoInfo
1111
Pull *models.Pull
1112
Stack models.Stack
···
1123
}
1124
1125
type RepoPullInterdiffParams struct {
1126
+
LoggedInUser *oauth.User
1127
RepoInfo repoinfo.RepoInfo
1128
Pull *models.Pull
1129
Round int
···
1176
}
1177
1178
type PullResubmitParams struct {
1179
+
LoggedInUser *oauth.User
1180
RepoInfo repoinfo.RepoInfo
1181
Pull *models.Pull
1182
SubmissionId int
···
1187
}
1188
1189
type PullActionsParams struct {
1190
+
LoggedInUser *oauth.User
1191
RepoInfo repoinfo.RepoInfo
1192
Pull *models.Pull
1193
RoundNumber int
···
1202
}
1203
1204
type PullNewCommentParams struct {
1205
+
LoggedInUser *oauth.User
1206
RepoInfo repoinfo.RepoInfo
1207
Pull *models.Pull
1208
RoundNumber int
···
1213
}
1214
1215
type RepoCompareParams struct {
1216
+
LoggedInUser *oauth.User
1217
RepoInfo repoinfo.RepoInfo
1218
Forks []models.Repo
1219
Branches []types.Branch
···
1232
}
1233
1234
type RepoCompareNewParams struct {
1235
+
LoggedInUser *oauth.User
1236
RepoInfo repoinfo.RepoInfo
1237
Forks []models.Repo
1238
Branches []types.Branch
···
1249
}
1250
1251
type RepoCompareAllowPullParams struct {
1252
+
LoggedInUser *oauth.User
1253
RepoInfo repoinfo.RepoInfo
1254
Base string
1255
Head string
···
1269
}
1270
1271
type LabelPanelParams struct {
1272
+
LoggedInUser *oauth.User
1273
RepoInfo repoinfo.RepoInfo
1274
Defs map[string]*models.LabelDefinition
1275
Subject string
···
1281
}
1282
1283
type EditLabelPanelParams struct {
1284
+
LoggedInUser *oauth.User
1285
RepoInfo repoinfo.RepoInfo
1286
Defs map[string]*models.LabelDefinition
1287
Subject string
···
1293
}
1294
1295
type PipelinesParams struct {
1296
+
LoggedInUser *oauth.User
1297
RepoInfo repoinfo.RepoInfo
1298
Pipelines []models.Pipeline
1299
Active string
···
1336
}
1337
1338
type WorkflowParams struct {
1339
+
LoggedInUser *oauth.User
1340
RepoInfo repoinfo.RepoInfo
1341
Pipeline models.Pipeline
1342
Workflow string
···
1350
}
1351
1352
type PutStringParams struct {
1353
+
LoggedInUser *oauth.User
1354
Action string
1355
1356
// this is supplied in the case of editing an existing string
···
1362
}
1363
1364
type StringsDashboardParams struct {
1365
+
LoggedInUser *oauth.User
1366
Card ProfileCard
1367
Strings []models.String
1368
}
···
1372
}
1373
1374
type StringTimelineParams struct {
1375
+
LoggedInUser *oauth.User
1376
Strings []models.String
1377
}
1378
···
1381
}
1382
1383
type SingleStringParams struct {
1384
+
LoggedInUser *oauth.User
1385
ShowRendered bool
1386
RenderToggle bool
1387
RenderedContents template.HTML
+1
-1
appview/pages/templates/fragments/dolly/silhouette.html
+1
-1
appview/pages/templates/fragments/dolly/silhouette.html
···
62
transform="translate(-0.42924038,-0.87777209)">
63
<path
64
class="dolly"
65
-
fill="currentColor"
66
style="stroke-width:0.111183"
67
d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z"
68
id="path7"
···
62
transform="translate(-0.42924038,-0.87777209)">
63
<path
64
class="dolly"
65
+
fill="{{ or . "currentColor" }}"
66
style="stroke-width:0.111183"
67
d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z"
68
id="path7"
+3
appview/pages/templates/layouts/base.html
+3
appview/pages/templates/layouts/base.html
···
11
<script defer src="/static/htmx-ext-ws.min.js"></script>
12
<script defer src="/static/actor-typeahead.js" type="module"></script>
13
14
+
<link rel="icon" href="/favicon.ico" sizes="48x48"/>
15
+
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml"/>
16
+
17
<!-- preconnect to image cdn -->
18
<link rel="preconnect" href="https://avatar.tangled.sh" />
19
<link rel="preconnect" href="https://camo.tangled.sh" />
+11
-49
appview/pages/templates/layouts/fragments/topbar.html
+11
-49
appview/pages/templates/layouts/fragments/topbar.html
···
49
{{ define "profileDropdown" }}
50
<details class="relative inline-block text-left nav-dropdown">
51
<summary class="cursor-pointer list-none flex items-center gap-1">
52
-
{{ $user := .Active.Did }}
53
<img
54
src="{{ tinyAvatar $user }}"
55
alt=""
···
57
/>
58
<span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span>
59
</summary>
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>
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>
109
</div>
110
</details>
111
···
49
{{ define "profileDropdown" }}
50
<details class="relative inline-block text-left nav-dropdown">
51
<summary class="cursor-pointer list-none flex items-center gap-1">
52
+
{{ $user := .Did }}
53
<img
54
src="{{ tinyAvatar $user }}"
55
alt=""
···
57
/>
58
<span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span>
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
70
</a>
71
</div>
72
</details>
73
-53
appview/pages/templates/user/login.html
-53
appview/pages/templates/user/login.html
···
20
<h2 class="text-center text-xl italic dark:text-white">
21
tightly-knit social coding.
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
-
73
<form
74
class="mt-4"
75
hx-post="/login"
···
96
</span>
97
</div>
98
<input type="hidden" name="return_url" value="{{ .ReturnUrl }}">
99
-
<input type="hidden" name="add_account" value="{{ if .AddAccount }}true{{ end }}">
100
101
<button
102
class="btn w-full my-2 mt-6 text-base "
···
117
You have not authorized the app.
118
{{ else if eq .ErrorCode "session" }}
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.
122
{{ else }}
123
Internal Server error.
124
{{ end }}
···
20
<h2 class="text-center text-xl italic dark:text-white">
21
tightly-knit social coding.
22
</h2>
23
<form
24
class="mt-4"
25
hx-post="/login"
···
46
</span>
47
</div>
48
<input type="hidden" name="return_url" value="{{ .ReturnUrl }}">
49
50
<button
51
class="btn w-full my-2 mt-6 text-base "
···
66
You have not authorized the app.
67
{{ else if eq .ErrorCode "session" }}
68
Server failed to create user session.
69
{{ else }}
70
Internal Server error.
71
{{ end }}
+2
-2
appview/pipelines/pipelines.go
+2
-2
appview/pipelines/pipelines.go
···
70
}
71
72
func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
73
-
user := p.oauth.GetMultiAccountUser(r)
74
l := p.logger.With("handler", "Index")
75
76
f, err := p.repoResolver.Resolve(r)
···
99
}
100
101
func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) {
102
-
user := p.oauth.GetMultiAccountUser(r)
103
l := p.logger.With("handler", "Workflow")
104
105
f, err := p.repoResolver.Resolve(r)
···
70
}
71
72
func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
73
+
user := p.oauth.GetUser(r)
74
l := p.logger.With("handler", "Index")
75
76
f, err := p.repoResolver.Resolve(r)
···
99
}
100
101
func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) {
102
+
user := p.oauth.GetUser(r)
103
l := p.logger.With("handler", "Workflow")
104
105
f, err := p.repoResolver.Resolve(r)
+55
-55
appview/pulls/pulls.go
+55
-55
appview/pulls/pulls.go
···
93
func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) {
94
switch r.Method {
95
case http.MethodGet:
96
-
user := s.oauth.GetMultiAccountUser(r)
97
f, err := s.repoResolver.Resolve(r)
98
if err != nil {
99
log.Println("failed to get repo and knot", err)
···
124
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
125
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
126
resubmitResult := pages.Unknown
127
-
if user.Active.Did == pull.OwnerDid {
128
resubmitResult = s.resubmitCheck(r, f, pull, stack)
129
}
130
···
143
}
144
145
func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
146
-
user := s.oauth.GetMultiAccountUser(r)
147
f, err := s.repoResolver.Resolve(r)
148
if err != nil {
149
log.Println("failed to get repo and knot", err)
···
171
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
172
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
173
resubmitResult := pages.Unknown
174
-
if user != nil && user.Active != nil && user.Active.Did == pull.OwnerDid {
175
resubmitResult = s.resubmitCheck(r, f, pull, stack)
176
}
177
···
213
214
userReactions := map[models.ReactionKind]bool{}
215
if user != nil {
216
-
userReactions = db.GetReactionStatusMap(s.db, user.Active.Did, pull.AtUri())
217
}
218
219
labelDefs, err := db.GetLabelDefinitions(
···
324
return nil
325
}
326
327
-
user := s.oauth.GetMultiAccountUser(r)
328
if user == nil {
329
return nil
330
}
···
347
}
348
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.Active.Did, repo.Knot, repo.DidSlashRepo())
351
if !slices.Contains(perms, "repo:push") {
352
return nil
353
}
···
434
}
435
436
func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
437
-
user := s.oauth.GetMultiAccountUser(r)
438
439
var diffOpts types.DiffOpts
440
if d := r.URL.Query().Get("diff"); d == "split" {
···
475
}
476
477
func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
478
-
user := s.oauth.GetMultiAccountUser(r)
479
480
var diffOpts types.DiffOpts
481
if d := r.URL.Query().Get("diff"); d == "split" {
···
520
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
521
522
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
523
-
LoggedInUser: s.oauth.GetMultiAccountUser(r),
524
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
525
Pull: pull,
526
Round: roundIdInt,
···
552
func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
553
l := s.logger.With("handler", "RepoPulls")
554
555
-
user := s.oauth.GetMultiAccountUser(r)
556
params := r.URL.Query()
557
558
state := models.PullOpen
···
680
}
681
682
s.pages.RepoPulls(w, pages.RepoPullsParams{
683
-
LoggedInUser: s.oauth.GetMultiAccountUser(r),
684
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
685
Pulls: pulls,
686
LabelDefs: defs,
···
692
}
693
694
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
695
-
user := s.oauth.GetMultiAccountUser(r)
696
f, err := s.repoResolver.Resolve(r)
697
if err != nil {
698
log.Println("failed to get repo and knot", err)
···
751
}
752
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
753
Collection: tangled.RepoPullCommentNSID,
754
-
Repo: user.Active.Did,
755
Rkey: tid.TID(),
756
Record: &lexutil.LexiconTypeDecoder{
757
Val: &tangled.RepoPullComment{
···
768
}
769
770
comment := &models.PullComment{
771
-
OwnerDid: user.Active.Did,
772
RepoAt: f.RepoAt().String(),
773
PullId: pull.PullId,
774
Body: body,
···
802
}
803
804
func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) {
805
-
user := s.oauth.GetMultiAccountUser(r)
806
f, err := s.repoResolver.Resolve(r)
807
if err != nil {
808
log.Println("failed to get repo and knot", err)
···
870
}
871
872
// Determine PR type based on input parameters
873
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
874
isPushAllowed := roles.IsPushAllowed()
875
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
876
isForkBased := fromFork != "" && sourceBranch != ""
···
970
w http.ResponseWriter,
971
r *http.Request,
972
repo *models.Repo,
973
-
user *oauth.MultiAccountUser,
974
title,
975
body,
976
targetBranch,
···
1027
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1028
}
1029
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
if err := s.validator.ValidatePatch(&patch); err != nil {
1032
s.logger.Error("patch validation failed", "err", err)
1033
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
···
1037
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked)
1038
}
1039
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
repoString := strings.SplitN(forkRepo, "/", 2)
1042
forkOwnerDid := repoString[0]
1043
repoName := repoString[1]
···
1146
w http.ResponseWriter,
1147
r *http.Request,
1148
repo *models.Repo,
1149
-
user *oauth.MultiAccountUser,
1150
title, body, targetBranch string,
1151
patch string,
1152
combined string,
···
1218
Title: title,
1219
Body: body,
1220
TargetBranch: targetBranch,
1221
-
OwnerDid: user.Active.Did,
1222
RepoAt: repo.RepoAt(),
1223
Rkey: rkey,
1224
Mentions: mentions,
···
1250
1251
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1252
Collection: tangled.RepoPullNSID,
1253
-
Repo: user.Active.Did,
1254
Rkey: rkey,
1255
Record: &lexutil.LexiconTypeDecoder{
1256
Val: &tangled.RepoPull{
···
1287
w http.ResponseWriter,
1288
r *http.Request,
1289
repo *models.Repo,
1290
-
user *oauth.MultiAccountUser,
1291
targetBranch string,
1292
patch string,
1293
sourceRev string,
···
1355
})
1356
}
1357
_, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
1358
-
Repo: user.Active.Did,
1359
Writes: writes,
1360
})
1361
if err != nil {
···
1427
}
1428
1429
func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1430
-
user := s.oauth.GetMultiAccountUser(r)
1431
1432
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1433
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
···
1435
}
1436
1437
func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
1438
-
user := s.oauth.GetMultiAccountUser(r)
1439
f, err := s.repoResolver.Resolve(r)
1440
if err != nil {
1441
log.Println("failed to get repo and knot", err)
···
1490
}
1491
1492
func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1493
-
user := s.oauth.GetMultiAccountUser(r)
1494
1495
-
forks, err := db.GetForksByDid(s.db, user.Active.Did)
1496
if err != nil {
1497
log.Println("failed to get forks", err)
1498
return
···
1506
}
1507
1508
func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1509
-
user := s.oauth.GetMultiAccountUser(r)
1510
1511
f, err := s.repoResolver.Resolve(r)
1512
if err != nil {
···
1599
}
1600
1601
func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1602
-
user := s.oauth.GetMultiAccountUser(r)
1603
1604
pull, ok := r.Context().Value("pull").(*models.Pull)
1605
if !ok {
···
1630
}
1631
1632
func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1633
-
user := s.oauth.GetMultiAccountUser(r)
1634
1635
pull, ok := r.Context().Value("pull").(*models.Pull)
1636
if !ok {
···
1645
return
1646
}
1647
1648
-
if user.Active.Did != pull.OwnerDid {
1649
log.Println("unauthorized user")
1650
w.WriteHeader(http.StatusUnauthorized)
1651
return
···
1657
}
1658
1659
func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1660
-
user := s.oauth.GetMultiAccountUser(r)
1661
1662
pull, ok := r.Context().Value("pull").(*models.Pull)
1663
if !ok {
···
1672
return
1673
}
1674
1675
-
if user.Active.Did != pull.OwnerDid {
1676
log.Println("unauthorized user")
1677
w.WriteHeader(http.StatusUnauthorized)
1678
return
1679
}
1680
1681
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
1682
if !roles.IsPushAllowed() {
1683
log.Println("unauthorized user")
1684
w.WriteHeader(http.StatusUnauthorized)
···
1722
}
1723
1724
func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) {
1725
-
user := s.oauth.GetMultiAccountUser(r)
1726
1727
pull, ok := r.Context().Value("pull").(*models.Pull)
1728
if !ok {
···
1737
return
1738
}
1739
1740
-
if user.Active.Did != pull.OwnerDid {
1741
log.Println("unauthorized user")
1742
w.WriteHeader(http.StatusUnauthorized)
1743
return
···
1822
w http.ResponseWriter,
1823
r *http.Request,
1824
repo *models.Repo,
1825
-
user *oauth.MultiAccountUser,
1826
pull *models.Pull,
1827
patch string,
1828
combined string,
···
1878
return
1879
}
1880
1881
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Active.Did, pull.Rkey)
1882
if err != nil {
1883
// failed to get record
1884
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
···
1897
1898
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1899
Collection: tangled.RepoPullNSID,
1900
-
Repo: user.Active.Did,
1901
Rkey: pull.Rkey,
1902
SwapRecord: ex.Cid,
1903
Record: &lexutil.LexiconTypeDecoder{
···
1924
w http.ResponseWriter,
1925
r *http.Request,
1926
repo *models.Repo,
1927
-
user *oauth.MultiAccountUser,
1928
pull *models.Pull,
1929
patch string,
1930
stackId string,
···
2114
}
2115
2116
_, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
2117
-
Repo: user.Active.Did,
2118
Writes: writes,
2119
})
2120
if err != nil {
···
2128
}
2129
2130
func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
2131
-
user := s.oauth.GetMultiAccountUser(r)
2132
f, err := s.repoResolver.Resolve(r)
2133
if err != nil {
2134
log.Println("failed to resolve repo:", err)
···
2239
2240
// notify about the pull merge
2241
for _, p := range pullsToMerge {
2242
-
s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p)
2243
}
2244
2245
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
···
2247
}
2248
2249
func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
2250
-
user := s.oauth.GetMultiAccountUser(r)
2251
2252
f, err := s.repoResolver.Resolve(r)
2253
if err != nil {
···
2263
}
2264
2265
// auth filter: only owner or collaborators can close
2266
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
2267
isOwner := roles.IsOwner()
2268
isCollaborator := roles.IsCollaborator()
2269
-
isPullAuthor := user.Active.Did == pull.OwnerDid
2270
isCloseAllowed := isOwner || isCollaborator || isPullAuthor
2271
if !isCloseAllowed {
2272
log.Println("failed to close pull")
···
2312
}
2313
2314
for _, p := range pullsToClose {
2315
-
s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p)
2316
}
2317
2318
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
···
2320
}
2321
2322
func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
2323
-
user := s.oauth.GetMultiAccountUser(r)
2324
2325
f, err := s.repoResolver.Resolve(r)
2326
if err != nil {
···
2337
}
2338
2339
// auth filter: only owner or collaborators can close
2340
-
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())}
2341
isOwner := roles.IsOwner()
2342
isCollaborator := roles.IsCollaborator()
2343
-
isPullAuthor := user.Active.Did == pull.OwnerDid
2344
isCloseAllowed := isOwner || isCollaborator || isPullAuthor
2345
if !isCloseAllowed {
2346
log.Println("failed to close pull")
···
2386
}
2387
2388
for _, p := range pullsToReopen {
2389
-
s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p)
2390
}
2391
2392
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2393
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2394
}
2395
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) {
2397
formatPatches, err := patchutil.ExtractPatches(patch)
2398
if err != nil {
2399
return nil, fmt.Errorf("Failed to extract patches: %v", err)
···
2429
Title: title,
2430
Body: body,
2431
TargetBranch: targetBranch,
2432
-
OwnerDid: user.Active.Did,
2433
RepoAt: repo.RepoAt(),
2434
Rkey: rkey,
2435
Mentions: mentions,
···
93
func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) {
94
switch r.Method {
95
case http.MethodGet:
96
+
user := s.oauth.GetUser(r)
97
f, err := s.repoResolver.Resolve(r)
98
if err != nil {
99
log.Println("failed to get repo and knot", err)
···
124
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
125
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
126
resubmitResult := pages.Unknown
127
+
if user.Did == pull.OwnerDid {
128
resubmitResult = s.resubmitCheck(r, f, pull, stack)
129
}
130
···
143
}
144
145
func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
146
+
user := s.oauth.GetUser(r)
147
f, err := s.repoResolver.Resolve(r)
148
if err != nil {
149
log.Println("failed to get repo and knot", err)
···
171
mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
172
branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
173
resubmitResult := pages.Unknown
174
+
if user != nil && user.Did == pull.OwnerDid {
175
resubmitResult = s.resubmitCheck(r, f, pull, stack)
176
}
177
···
213
214
userReactions := map[models.ReactionKind]bool{}
215
if user != nil {
216
+
userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.AtUri())
217
}
218
219
labelDefs, err := db.GetLabelDefinitions(
···
324
return nil
325
}
326
327
+
user := s.oauth.GetUser(r)
328
if user == nil {
329
return nil
330
}
···
347
}
348
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())
351
if !slices.Contains(perms, "repo:push") {
352
return nil
353
}
···
434
}
435
436
func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
437
+
user := s.oauth.GetUser(r)
438
439
var diffOpts types.DiffOpts
440
if d := r.URL.Query().Get("diff"); d == "split" {
···
475
}
476
477
func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
478
+
user := s.oauth.GetUser(r)
479
480
var diffOpts types.DiffOpts
481
if d := r.URL.Query().Get("diff"); d == "split" {
···
520
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
521
522
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
523
+
LoggedInUser: s.oauth.GetUser(r),
524
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
525
Pull: pull,
526
Round: roundIdInt,
···
552
func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
553
l := s.logger.With("handler", "RepoPulls")
554
555
+
user := s.oauth.GetUser(r)
556
params := r.URL.Query()
557
558
state := models.PullOpen
···
680
}
681
682
s.pages.RepoPulls(w, pages.RepoPullsParams{
683
+
LoggedInUser: s.oauth.GetUser(r),
684
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
685
Pulls: pulls,
686
LabelDefs: defs,
···
692
}
693
694
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
695
+
user := s.oauth.GetUser(r)
696
f, err := s.repoResolver.Resolve(r)
697
if err != nil {
698
log.Println("failed to get repo and knot", err)
···
751
}
752
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
753
Collection: tangled.RepoPullCommentNSID,
754
+
Repo: user.Did,
755
Rkey: tid.TID(),
756
Record: &lexutil.LexiconTypeDecoder{
757
Val: &tangled.RepoPullComment{
···
768
}
769
770
comment := &models.PullComment{
771
+
OwnerDid: user.Did,
772
RepoAt: f.RepoAt().String(),
773
PullId: pull.PullId,
774
Body: body,
···
802
}
803
804
func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) {
805
+
user := s.oauth.GetUser(r)
806
f, err := s.repoResolver.Resolve(r)
807
if err != nil {
808
log.Println("failed to get repo and knot", err)
···
870
}
871
872
// Determine PR type based on input parameters
873
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
874
isPushAllowed := roles.IsPushAllowed()
875
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
876
isForkBased := fromFork != "" && sourceBranch != ""
···
970
w http.ResponseWriter,
971
r *http.Request,
972
repo *models.Repo,
973
+
user *oauth.User,
974
title,
975
body,
976
targetBranch,
···
1027
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1028
}
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) {
1031
if err := s.validator.ValidatePatch(&patch); err != nil {
1032
s.logger.Error("patch validation failed", "err", err)
1033
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
···
1037
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked)
1038
}
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) {
1041
repoString := strings.SplitN(forkRepo, "/", 2)
1042
forkOwnerDid := repoString[0]
1043
repoName := repoString[1]
···
1146
w http.ResponseWriter,
1147
r *http.Request,
1148
repo *models.Repo,
1149
+
user *oauth.User,
1150
title, body, targetBranch string,
1151
patch string,
1152
combined string,
···
1218
Title: title,
1219
Body: body,
1220
TargetBranch: targetBranch,
1221
+
OwnerDid: user.Did,
1222
RepoAt: repo.RepoAt(),
1223
Rkey: rkey,
1224
Mentions: mentions,
···
1250
1251
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1252
Collection: tangled.RepoPullNSID,
1253
+
Repo: user.Did,
1254
Rkey: rkey,
1255
Record: &lexutil.LexiconTypeDecoder{
1256
Val: &tangled.RepoPull{
···
1287
w http.ResponseWriter,
1288
r *http.Request,
1289
repo *models.Repo,
1290
+
user *oauth.User,
1291
targetBranch string,
1292
patch string,
1293
sourceRev string,
···
1355
})
1356
}
1357
_, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
1358
+
Repo: user.Did,
1359
Writes: writes,
1360
})
1361
if err != nil {
···
1427
}
1428
1429
func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1430
+
user := s.oauth.GetUser(r)
1431
1432
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1433
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
···
1435
}
1436
1437
func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
1438
+
user := s.oauth.GetUser(r)
1439
f, err := s.repoResolver.Resolve(r)
1440
if err != nil {
1441
log.Println("failed to get repo and knot", err)
···
1490
}
1491
1492
func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1493
+
user := s.oauth.GetUser(r)
1494
1495
+
forks, err := db.GetForksByDid(s.db, user.Did)
1496
if err != nil {
1497
log.Println("failed to get forks", err)
1498
return
···
1506
}
1507
1508
func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1509
+
user := s.oauth.GetUser(r)
1510
1511
f, err := s.repoResolver.Resolve(r)
1512
if err != nil {
···
1599
}
1600
1601
func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1602
+
user := s.oauth.GetUser(r)
1603
1604
pull, ok := r.Context().Value("pull").(*models.Pull)
1605
if !ok {
···
1630
}
1631
1632
func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1633
+
user := s.oauth.GetUser(r)
1634
1635
pull, ok := r.Context().Value("pull").(*models.Pull)
1636
if !ok {
···
1645
return
1646
}
1647
1648
+
if user.Did != pull.OwnerDid {
1649
log.Println("unauthorized user")
1650
w.WriteHeader(http.StatusUnauthorized)
1651
return
···
1657
}
1658
1659
func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1660
+
user := s.oauth.GetUser(r)
1661
1662
pull, ok := r.Context().Value("pull").(*models.Pull)
1663
if !ok {
···
1672
return
1673
}
1674
1675
+
if user.Did != pull.OwnerDid {
1676
log.Println("unauthorized user")
1677
w.WriteHeader(http.StatusUnauthorized)
1678
return
1679
}
1680
1681
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
1682
if !roles.IsPushAllowed() {
1683
log.Println("unauthorized user")
1684
w.WriteHeader(http.StatusUnauthorized)
···
1722
}
1723
1724
func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) {
1725
+
user := s.oauth.GetUser(r)
1726
1727
pull, ok := r.Context().Value("pull").(*models.Pull)
1728
if !ok {
···
1737
return
1738
}
1739
1740
+
if user.Did != pull.OwnerDid {
1741
log.Println("unauthorized user")
1742
w.WriteHeader(http.StatusUnauthorized)
1743
return
···
1822
w http.ResponseWriter,
1823
r *http.Request,
1824
repo *models.Repo,
1825
+
user *oauth.User,
1826
pull *models.Pull,
1827
patch string,
1828
combined string,
···
1878
return
1879
}
1880
1881
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1882
if err != nil {
1883
// failed to get record
1884
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
···
1897
1898
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1899
Collection: tangled.RepoPullNSID,
1900
+
Repo: user.Did,
1901
Rkey: pull.Rkey,
1902
SwapRecord: ex.Cid,
1903
Record: &lexutil.LexiconTypeDecoder{
···
1924
w http.ResponseWriter,
1925
r *http.Request,
1926
repo *models.Repo,
1927
+
user *oauth.User,
1928
pull *models.Pull,
1929
patch string,
1930
stackId string,
···
2114
}
2115
2116
_, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
2117
+
Repo: user.Did,
2118
Writes: writes,
2119
})
2120
if err != nil {
···
2128
}
2129
2130
func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
2131
+
user := s.oauth.GetUser(r)
2132
f, err := s.repoResolver.Resolve(r)
2133
if err != nil {
2134
log.Println("failed to resolve repo:", err)
···
2239
2240
// notify about the pull merge
2241
for _, p := range pullsToMerge {
2242
+
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2243
}
2244
2245
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
···
2247
}
2248
2249
func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
2250
+
user := s.oauth.GetUser(r)
2251
2252
f, err := s.repoResolver.Resolve(r)
2253
if err != nil {
···
2263
}
2264
2265
// auth filter: only owner or collaborators can close
2266
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
2267
isOwner := roles.IsOwner()
2268
isCollaborator := roles.IsCollaborator()
2269
+
isPullAuthor := user.Did == pull.OwnerDid
2270
isCloseAllowed := isOwner || isCollaborator || isPullAuthor
2271
if !isCloseAllowed {
2272
log.Println("failed to close pull")
···
2312
}
2313
2314
for _, p := range pullsToClose {
2315
+
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2316
}
2317
2318
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
···
2320
}
2321
2322
func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
2323
+
user := s.oauth.GetUser(r)
2324
2325
f, err := s.repoResolver.Resolve(r)
2326
if err != nil {
···
2337
}
2338
2339
// auth filter: only owner or collaborators can close
2340
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
2341
isOwner := roles.IsOwner()
2342
isCollaborator := roles.IsCollaborator()
2343
+
isPullAuthor := user.Did == pull.OwnerDid
2344
isCloseAllowed := isOwner || isCollaborator || isPullAuthor
2345
if !isCloseAllowed {
2346
log.Println("failed to close pull")
···
2386
}
2387
2388
for _, p := range pullsToReopen {
2389
+
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2390
}
2391
2392
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2393
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2394
}
2395
2396
+
func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
2397
formatPatches, err := patchutil.ExtractPatches(patch)
2398
if err != nil {
2399
return nil, fmt.Errorf("Failed to extract patches: %v", err)
···
2429
Title: title,
2430
Body: body,
2431
TargetBranch: targetBranch,
2432
+
OwnerDid: user.Did,
2433
RepoAt: repo.RepoAt(),
2434
Rkey: rkey,
2435
Mentions: mentions,
+6
-6
appview/repo/artifact.go
+6
-6
appview/repo/artifact.go
···
30
31
// TODO: proper statuses here on early exit
32
func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) {
33
-
user := rp.oauth.GetMultiAccountUser(r)
34
tagParam := chi.URLParam(r, "tag")
35
f, err := rp.repoResolver.Resolve(r)
36
if err != nil {
···
75
76
putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
77
Collection: tangled.RepoArtifactNSID,
78
-
Repo: user.Active.Did,
79
Rkey: rkey,
80
Record: &lexutil.LexiconTypeDecoder{
81
Val: &tangled.RepoArtifact{
···
104
defer tx.Rollback()
105
106
artifact := models.Artifact{
107
-
Did: user.Active.Did,
108
Rkey: rkey,
109
RepoAt: f.RepoAt(),
110
Tag: tag.Tag.Hash,
···
220
221
// TODO: proper statuses here on early exit
222
func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
223
-
user := rp.oauth.GetMultiAccountUser(r)
224
tagParam := chi.URLParam(r, "tag")
225
filename := chi.URLParam(r, "file")
226
f, err := rp.repoResolver.Resolve(r)
···
251
252
artifact := artifacts[0]
253
254
-
if user.Active.Did != artifact.Did {
255
log.Println("user not authorized to delete artifact", err)
256
rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.")
257
return
···
259
260
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
261
Collection: tangled.RepoArtifactNSID,
262
-
Repo: user.Active.Did,
263
Rkey: artifact.Rkey,
264
})
265
if err != nil {
···
30
31
// TODO: proper statuses here on early exit
32
func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) {
33
+
user := rp.oauth.GetUser(r)
34
tagParam := chi.URLParam(r, "tag")
35
f, err := rp.repoResolver.Resolve(r)
36
if err != nil {
···
75
76
putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
77
Collection: tangled.RepoArtifactNSID,
78
+
Repo: user.Did,
79
Rkey: rkey,
80
Record: &lexutil.LexiconTypeDecoder{
81
Val: &tangled.RepoArtifact{
···
104
defer tx.Rollback()
105
106
artifact := models.Artifact{
107
+
Did: user.Did,
108
Rkey: rkey,
109
RepoAt: f.RepoAt(),
110
Tag: tag.Tag.Hash,
···
220
221
// TODO: proper statuses here on early exit
222
func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
223
+
user := rp.oauth.GetUser(r)
224
tagParam := chi.URLParam(r, "tag")
225
filename := chi.URLParam(r, "file")
226
f, err := rp.repoResolver.Resolve(r)
···
251
252
artifact := artifacts[0]
253
254
+
if user.Did != artifact.Did {
255
log.Println("user not authorized to delete artifact", err)
256
rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.")
257
return
···
259
260
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
261
Collection: tangled.RepoArtifactNSID,
262
+
Repo: user.Did,
263
Rkey: artifact.Rkey,
264
})
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
func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) {
21
l := rp.logger.With("handler", "RepoCompareNew")
22
23
-
user := rp.oauth.GetMultiAccountUser(r)
24
f, err := rp.repoResolver.Resolve(r)
25
if err != nil {
26
l.Error("failed to get repo and knot", "err", err)
···
101
func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) {
102
l := rp.logger.With("handler", "RepoCompare")
103
104
-
user := rp.oauth.GetMultiAccountUser(r)
105
f, err := rp.repoResolver.Resolve(r)
106
if err != nil {
107
l.Error("failed to get repo and knot", "err", err)
···
20
func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) {
21
l := rp.logger.With("handler", "RepoCompareNew")
22
23
+
user := rp.oauth.GetUser(r)
24
f, err := rp.repoResolver.Resolve(r)
25
if err != nil {
26
l.Error("failed to get repo and knot", "err", err)
···
101
func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) {
102
l := rp.logger.With("handler", "RepoCompare")
103
104
+
user := rp.oauth.GetUser(r)
105
f, err := rp.repoResolver.Resolve(r)
106
if err != nil {
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
}
110
}
111
112
-
user := rp.oauth.GetMultiAccountUser(r)
113
114
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true)
115
if err != nil {
···
197
l.Error("failed to GetVerifiedCommits", "err", err)
198
}
199
200
-
user := rp.oauth.GetMultiAccountUser(r)
201
pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This})
202
if err != nil {
203
l.Error("failed to getPipelineStatuses", "err", err)
···
109
}
110
}
111
112
+
user := rp.oauth.GetUser(r)
113
114
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true)
115
if err != nil {
···
197
l.Error("failed to GetVerifiedCommits", "err", err)
198
}
199
200
+
user := rp.oauth.GetUser(r)
201
pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This})
202
if err != nil {
203
l.Error("failed to getPipelineStatuses", "err", err)
+34
-34
appview/repo/repo.go
+34
-34
appview/repo/repo.go
···
81
82
// modify the spindle configured for this repo
83
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
84
-
user := rp.oauth.GetMultiAccountUser(r)
85
l := rp.logger.With("handler", "EditSpindle")
86
-
l = l.With("did", user.Active.Did)
87
88
errorId := "operation-error"
89
fail := func(msg string, err error) {
···
107
108
if !removingSpindle {
109
// ensure that this is a valid spindle for this user
110
-
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Active.Did)
111
if err != nil {
112
fail("Failed to find spindles. Try again later.", err)
113
return
···
168
}
169
170
func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) {
171
-
user := rp.oauth.GetMultiAccountUser(r)
172
l := rp.logger.With("handler", "AddLabel")
173
-
l = l.With("did", user.Active.Did)
174
175
f, err := rp.repoResolver.Resolve(r)
176
if err != nil {
···
216
}
217
218
label := models.LabelDefinition{
219
-
Did: user.Active.Did,
220
Rkey: tid.TID(),
221
Name: name,
222
ValueType: valueType,
···
327
}
328
329
func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) {
330
-
user := rp.oauth.GetMultiAccountUser(r)
331
l := rp.logger.With("handler", "DeleteLabel")
332
-
l = l.With("did", user.Active.Did)
333
334
f, err := rp.repoResolver.Resolve(r)
335
if err != nil {
···
435
}
436
437
func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) {
438
-
user := rp.oauth.GetMultiAccountUser(r)
439
l := rp.logger.With("handler", "SubscribeLabel")
440
-
l = l.With("did", user.Active.Did)
441
442
f, err := rp.repoResolver.Resolve(r)
443
if err != nil {
···
521
}
522
523
func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) {
524
-
user := rp.oauth.GetMultiAccountUser(r)
525
l := rp.logger.With("handler", "UnsubscribeLabel")
526
-
l = l.With("did", user.Active.Did)
527
528
f, err := rp.repoResolver.Resolve(r)
529
if err != nil {
···
633
}
634
state := states[subject]
635
636
-
user := rp.oauth.GetMultiAccountUser(r)
637
rp.pages.LabelPanel(w, pages.LabelPanelParams{
638
LoggedInUser: user,
639
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
···
681
}
682
state := states[subject]
683
684
-
user := rp.oauth.GetMultiAccountUser(r)
685
rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
686
LoggedInUser: user,
687
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
···
692
}
693
694
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
695
-
user := rp.oauth.GetMultiAccountUser(r)
696
l := rp.logger.With("handler", "AddCollaborator")
697
-
l = l.With("did", user.Active.Did)
698
699
f, err := rp.repoResolver.Resolve(r)
700
if err != nil {
···
723
return
724
}
725
726
-
if collaboratorIdent.DID.String() == user.Active.Did {
727
fail("You seem to be adding yourself as a collaborator.", nil)
728
return
729
}
···
738
}
739
740
// emit a record
741
-
currentUser := rp.oauth.GetMultiAccountUser(r)
742
rkey := tid.TID()
743
createdAt := time.Now()
744
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
745
Collection: tangled.RepoCollaboratorNSID,
746
-
Repo: currentUser.Active.Did,
747
Rkey: rkey,
748
Record: &lexutil.LexiconTypeDecoder{
749
Val: &tangled.RepoCollaborator{
···
792
}
793
794
err = db.AddCollaborator(tx, models.Collaborator{
795
-
Did: syntax.DID(currentUser.Active.Did),
796
Rkey: rkey,
797
SubjectDid: collaboratorIdent.DID,
798
RepoAt: f.RepoAt(),
···
822
}
823
824
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
825
-
user := rp.oauth.GetMultiAccountUser(r)
826
l := rp.logger.With("handler", "DeleteRepo")
827
828
noticeId := "operation-error"
···
840
}
841
_, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{
842
Collection: tangled.RepoNSID,
843
-
Repo: user.Active.Did,
844
Rkey: f.Rkey,
845
})
846
if err != nil {
···
940
ref := chi.URLParam(r, "ref")
941
ref, _ = url.PathUnescape(ref)
942
943
-
user := rp.oauth.GetMultiAccountUser(r)
944
f, err := rp.repoResolver.Resolve(r)
945
if err != nil {
946
l.Error("failed to resolve source repo", "err", err)
···
969
r.Context(),
970
client,
971
&tangled.RepoForkSync_Input{
972
-
Did: user.Active.Did,
973
Name: f.Name,
974
Source: f.Source,
975
Branch: ref,
···
988
func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) {
989
l := rp.logger.With("handler", "ForkRepo")
990
991
-
user := rp.oauth.GetMultiAccountUser(r)
992
f, err := rp.repoResolver.Resolve(r)
993
if err != nil {
994
l.Error("failed to resolve source repo", "err", err)
···
997
998
switch r.Method {
999
case http.MethodGet:
1000
-
user := rp.oauth.GetMultiAccountUser(r)
1001
-
knots, err := rp.enforcer.GetKnotsForUser(user.Active.Did)
1002
if err != nil {
1003
rp.pages.Notice(w, "repo", "Invalid user account.")
1004
return
···
1020
}
1021
l = l.With("targetKnot", targetKnot)
1022
1023
-
ok, err := rp.enforcer.E.Enforce(user.Active.Did, targetKnot, targetKnot, "repo:create")
1024
if err != nil || !ok {
1025
rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
1026
return
···
1037
// in the user's account.
1038
existingRepo, err := db.GetRepo(
1039
rp.db,
1040
-
orm.FilterEq("did", user.Active.Did),
1041
orm.FilterEq("name", forkName),
1042
)
1043
if err != nil {
···
1066
// create an atproto record for this fork
1067
rkey := tid.TID()
1068
repo := &models.Repo{
1069
-
Did: user.Active.Did,
1070
Name: forkName,
1071
Knot: targetKnot,
1072
Rkey: rkey,
···
1086
1087
atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{
1088
Collection: tangled.RepoNSID,
1089
-
Repo: user.Active.Did,
1090
Rkey: rkey,
1091
Record: &lexutil.LexiconTypeDecoder{
1092
Val: &record,
···
1165
}
1166
1167
// acls
1168
-
p, _ := securejoin.SecureJoin(user.Active.Did, forkName)
1169
-
err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p)
1170
if err != nil {
1171
l.Error("failed to add ACLs", "err", err)
1172
rp.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
1191
aturi = ""
1192
1193
rp.notifier.NewRepo(r.Context(), repo)
1194
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName))
1195
}
1196
}
1197
···
81
82
// modify the spindle configured for this repo
83
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
84
+
user := rp.oauth.GetUser(r)
85
l := rp.logger.With("handler", "EditSpindle")
86
+
l = l.With("did", user.Did)
87
88
errorId := "operation-error"
89
fail := func(msg string, err error) {
···
107
108
if !removingSpindle {
109
// ensure that this is a valid spindle for this user
110
+
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
111
if err != nil {
112
fail("Failed to find spindles. Try again later.", err)
113
return
···
168
}
169
170
func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) {
171
+
user := rp.oauth.GetUser(r)
172
l := rp.logger.With("handler", "AddLabel")
173
+
l = l.With("did", user.Did)
174
175
f, err := rp.repoResolver.Resolve(r)
176
if err != nil {
···
216
}
217
218
label := models.LabelDefinition{
219
+
Did: user.Did,
220
Rkey: tid.TID(),
221
Name: name,
222
ValueType: valueType,
···
327
}
328
329
func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) {
330
+
user := rp.oauth.GetUser(r)
331
l := rp.logger.With("handler", "DeleteLabel")
332
+
l = l.With("did", user.Did)
333
334
f, err := rp.repoResolver.Resolve(r)
335
if err != nil {
···
435
}
436
437
func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) {
438
+
user := rp.oauth.GetUser(r)
439
l := rp.logger.With("handler", "SubscribeLabel")
440
+
l = l.With("did", user.Did)
441
442
f, err := rp.repoResolver.Resolve(r)
443
if err != nil {
···
521
}
522
523
func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) {
524
+
user := rp.oauth.GetUser(r)
525
l := rp.logger.With("handler", "UnsubscribeLabel")
526
+
l = l.With("did", user.Did)
527
528
f, err := rp.repoResolver.Resolve(r)
529
if err != nil {
···
633
}
634
state := states[subject]
635
636
+
user := rp.oauth.GetUser(r)
637
rp.pages.LabelPanel(w, pages.LabelPanelParams{
638
LoggedInUser: user,
639
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
···
681
}
682
state := states[subject]
683
684
+
user := rp.oauth.GetUser(r)
685
rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
686
LoggedInUser: user,
687
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
···
692
}
693
694
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
695
+
user := rp.oauth.GetUser(r)
696
l := rp.logger.With("handler", "AddCollaborator")
697
+
l = l.With("did", user.Did)
698
699
f, err := rp.repoResolver.Resolve(r)
700
if err != nil {
···
723
return
724
}
725
726
+
if collaboratorIdent.DID.String() == user.Did {
727
fail("You seem to be adding yourself as a collaborator.", nil)
728
return
729
}
···
738
}
739
740
// emit a record
741
+
currentUser := rp.oauth.GetUser(r)
742
rkey := tid.TID()
743
createdAt := time.Now()
744
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
745
Collection: tangled.RepoCollaboratorNSID,
746
+
Repo: currentUser.Did,
747
Rkey: rkey,
748
Record: &lexutil.LexiconTypeDecoder{
749
Val: &tangled.RepoCollaborator{
···
792
}
793
794
err = db.AddCollaborator(tx, models.Collaborator{
795
+
Did: syntax.DID(currentUser.Did),
796
Rkey: rkey,
797
SubjectDid: collaboratorIdent.DID,
798
RepoAt: f.RepoAt(),
···
822
}
823
824
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
825
+
user := rp.oauth.GetUser(r)
826
l := rp.logger.With("handler", "DeleteRepo")
827
828
noticeId := "operation-error"
···
840
}
841
_, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{
842
Collection: tangled.RepoNSID,
843
+
Repo: user.Did,
844
Rkey: f.Rkey,
845
})
846
if err != nil {
···
940
ref := chi.URLParam(r, "ref")
941
ref, _ = url.PathUnescape(ref)
942
943
+
user := rp.oauth.GetUser(r)
944
f, err := rp.repoResolver.Resolve(r)
945
if err != nil {
946
l.Error("failed to resolve source repo", "err", err)
···
969
r.Context(),
970
client,
971
&tangled.RepoForkSync_Input{
972
+
Did: user.Did,
973
Name: f.Name,
974
Source: f.Source,
975
Branch: ref,
···
988
func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) {
989
l := rp.logger.With("handler", "ForkRepo")
990
991
+
user := rp.oauth.GetUser(r)
992
f, err := rp.repoResolver.Resolve(r)
993
if err != nil {
994
l.Error("failed to resolve source repo", "err", err)
···
997
998
switch r.Method {
999
case http.MethodGet:
1000
+
user := rp.oauth.GetUser(r)
1001
+
knots, err := rp.enforcer.GetKnotsForUser(user.Did)
1002
if err != nil {
1003
rp.pages.Notice(w, "repo", "Invalid user account.")
1004
return
···
1020
}
1021
l = l.With("targetKnot", targetKnot)
1022
1023
+
ok, err := rp.enforcer.E.Enforce(user.Did, targetKnot, targetKnot, "repo:create")
1024
if err != nil || !ok {
1025
rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
1026
return
···
1037
// in the user's account.
1038
existingRepo, err := db.GetRepo(
1039
rp.db,
1040
+
orm.FilterEq("did", user.Did),
1041
orm.FilterEq("name", forkName),
1042
)
1043
if err != nil {
···
1066
// create an atproto record for this fork
1067
rkey := tid.TID()
1068
repo := &models.Repo{
1069
+
Did: user.Did,
1070
Name: forkName,
1071
Knot: targetKnot,
1072
Rkey: rkey,
···
1086
1087
atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{
1088
Collection: tangled.RepoNSID,
1089
+
Repo: user.Did,
1090
Rkey: rkey,
1091
Record: &lexutil.LexiconTypeDecoder{
1092
Val: &record,
···
1165
}
1166
1167
// acls
1168
+
p, _ := securejoin.SecureJoin(user.Did, forkName)
1169
+
err = rp.enforcer.AddRepo(user.Did, targetKnot, p)
1170
if err != nil {
1171
l.Error("failed to add ACLs", "err", err)
1172
rp.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
1191
aturi = ""
1192
1193
rp.notifier.NewRepo(r.Context(), repo)
1194
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, forkName))
1195
}
1196
}
1197
+5
-5
appview/repo/settings.go
+5
-5
appview/repo/settings.go
···
79
}
80
81
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
82
-
user := rp.oauth.GetMultiAccountUser(r)
83
l := rp.logger.With("handler", "Secrets")
84
-
l = l.With("did", user.Active.Did)
85
86
f, err := rp.repoResolver.Resolve(r)
87
if err != nil {
···
185
l := rp.logger.With("handler", "generalSettings")
186
187
f, err := rp.repoResolver.Resolve(r)
188
-
user := rp.oauth.GetMultiAccountUser(r)
189
190
scheme := "http"
191
if !rp.config.Core.Dev {
···
271
l := rp.logger.With("handler", "accessSettings")
272
273
f, err := rp.repoResolver.Resolve(r)
274
-
user := rp.oauth.GetMultiAccountUser(r)
275
276
collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) {
277
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot)
···
318
l := rp.logger.With("handler", "pipelineSettings")
319
320
f, err := rp.repoResolver.Resolve(r)
321
-
user := rp.oauth.GetMultiAccountUser(r)
322
323
// all spindles that the repo owner is a member of
324
spindles, err := rp.enforcer.GetSpindlesForUser(f.Did)
···
79
}
80
81
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
82
+
user := rp.oauth.GetUser(r)
83
l := rp.logger.With("handler", "Secrets")
84
+
l = l.With("did", user.Did)
85
86
f, err := rp.repoResolver.Resolve(r)
87
if err != nil {
···
185
l := rp.logger.With("handler", "generalSettings")
186
187
f, err := rp.repoResolver.Resolve(r)
188
+
user := rp.oauth.GetUser(r)
189
190
scheme := "http"
191
if !rp.config.Core.Dev {
···
271
l := rp.logger.With("handler", "accessSettings")
272
273
f, err := rp.repoResolver.Resolve(r)
274
+
user := rp.oauth.GetUser(r)
275
276
collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) {
277
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot)
···
318
l := rp.logger.With("handler", "pipelineSettings")
319
320
f, err := rp.repoResolver.Resolve(r)
321
+
user := rp.oauth.GetUser(r)
322
323
// all spindles that the repo owner is a member of
324
spindles, err := rp.enforcer.GetSpindlesForUser(f.Did)
+1
-1
appview/repo/tree.go
+1
-1
appview/repo/tree.go
+5
-30
appview/reporesolver/resolver.go
+5
-30
appview/reporesolver/resolver.go
···
55
// 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo`
56
// 3. [x] remove `ResolvedRepo`
57
// 4. [ ] replace reporesolver to reposervice
58
-
func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.MultiAccountUser) repoinfo.RepoInfo {
59
ownerId, ook := r.Context().Value("resolvedId").(identity.Identity)
60
repo, rok := r.Context().Value("repo").(*models.Repo)
61
if !ook || !rok {
···
63
}
64
65
// get dir/ref
66
-
currentDir := extractCurrentDir(r.URL.EscapedPath())
67
ref := chi.URLParam(r, "ref")
68
69
repoAt := repo.RepoAt()
70
isStarred := false
71
roles := repoinfo.RolesInRepo{}
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
}
76
77
stats := repo.RepoStats
···
130
}
131
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 "."
158
}
159
160
// extractPathAfterRef gets the actual repository path
···
55
// 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo`
56
// 3. [x] remove `ResolvedRepo`
57
// 4. [ ] replace reporesolver to reposervice
58
+
func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.User) repoinfo.RepoInfo {
59
ownerId, ook := r.Context().Value("resolvedId").(identity.Identity)
60
repo, rok := r.Context().Value("repo").(*models.Repo)
61
if !ook || !rok {
···
63
}
64
65
// get dir/ref
66
+
currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath()))
67
ref := chi.URLParam(r, "ref")
68
69
repoAt := repo.RepoAt()
70
isStarred := false
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())
75
}
76
77
stats := repo.RepoStats
···
130
}
131
132
return repoInfo
133
}
134
135
// 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
}
82
83
func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) {
84
-
user := s.OAuth.GetMultiAccountUser(r)
85
86
s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{
87
LoggedInUser: user,
···
91
}
92
93
func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) {
94
-
user := s.OAuth.GetMultiAccountUser(r)
95
did := s.OAuth.GetDid(r)
96
97
prefs, err := db.GetNotificationPreference(s.Db, did)
···
137
}
138
139
func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) {
140
-
user := s.OAuth.GetMultiAccountUser(r)
141
-
pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Active.Did)
142
if err != nil {
143
log.Println(err)
144
}
···
152
}
153
154
func (s *Settings) emailsSettings(w http.ResponseWriter, r *http.Request) {
155
-
user := s.OAuth.GetMultiAccountUser(r)
156
-
emails, err := db.GetAllEmails(s.Db, user.Active.Did)
157
if err != nil {
158
log.Println(err)
159
}
···
81
}
82
83
func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) {
84
+
user := s.OAuth.GetUser(r)
85
86
s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{
87
LoggedInUser: user,
···
91
}
92
93
func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) {
94
+
user := s.OAuth.GetUser(r)
95
did := s.OAuth.GetDid(r)
96
97
prefs, err := db.GetNotificationPreference(s.Db, did)
···
137
}
138
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)
142
if err != nil {
143
log.Println(err)
144
}
···
152
}
153
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)
157
if err != nil {
158
log.Println(err)
159
}
+41
-41
appview/spindles/spindles.go
+41
-41
appview/spindles/spindles.go
···
69
}
70
71
func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) {
72
-
user := s.OAuth.GetMultiAccountUser(r)
73
all, err := db.GetSpindles(
74
s.Db,
75
-
orm.FilterEq("owner", user.Active.Did),
76
)
77
if err != nil {
78
s.Logger.Error("failed to fetch spindles", "err", err)
···
91
func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) {
92
l := s.Logger.With("handler", "dashboard")
93
94
-
user := s.OAuth.GetMultiAccountUser(r)
95
-
l = l.With("user", user.Active.Did)
96
97
instance := chi.URLParam(r, "instance")
98
if instance == "" {
···
103
spindles, err := db.GetSpindles(
104
s.Db,
105
orm.FilterEq("instance", instance),
106
-
orm.FilterEq("owner", user.Active.Did),
107
orm.FilterIsNot("verified", "null"),
108
)
109
if err != nil || len(spindles) != 1 {
···
155
//
156
// if the spindle is not up yet, the user is free to retry verification at a later point
157
func (s *Spindles) register(w http.ResponseWriter, r *http.Request) {
158
-
user := s.OAuth.GetMultiAccountUser(r)
159
l := s.Logger.With("handler", "register")
160
161
noticeId := "register-error"
···
176
return
177
}
178
l = l.With("instance", instance)
179
-
l = l.With("user", user.Active.Did)
180
181
tx, err := s.Db.Begin()
182
if err != nil {
···
190
}()
191
192
err = db.AddSpindle(tx, models.Spindle{
193
-
Owner: syntax.DID(user.Active.Did),
194
Instance: instance,
195
})
196
if err != nil {
···
214
return
215
}
216
217
-
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Active.Did, instance)
218
var exCid *string
219
if ex != nil {
220
exCid = ex.Cid
···
223
// re-announce by registering under same rkey
224
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
225
Collection: tangled.SpindleNSID,
226
-
Repo: user.Active.Did,
227
Rkey: instance,
228
Record: &lexutil.LexiconTypeDecoder{
229
Val: &tangled.Spindle{
···
254
}
255
256
// begin verification
257
-
err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev)
258
if err != nil {
259
l.Error("verification failed", "err", err)
260
s.Pages.HxRefresh(w)
261
return
262
}
263
264
-
_, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did)
265
if err != nil {
266
l.Error("failed to mark verified", "err", err)
267
s.Pages.HxRefresh(w)
···
273
}
274
275
func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) {
276
-
user := s.OAuth.GetMultiAccountUser(r)
277
l := s.Logger.With("handler", "delete")
278
279
noticeId := "operation-error"
···
291
292
spindles, err := db.GetSpindles(
293
s.Db,
294
-
orm.FilterEq("owner", user.Active.Did),
295
orm.FilterEq("instance", instance),
296
)
297
if err != nil || len(spindles) != 1 {
···
300
return
301
}
302
303
-
if string(spindles[0].Owner) != user.Active.Did {
304
-
l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner)
305
s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.")
306
return
307
}
···
320
// remove spindle members first
321
err = db.RemoveSpindleMember(
322
tx,
323
-
orm.FilterEq("did", user.Active.Did),
324
orm.FilterEq("instance", instance),
325
)
326
if err != nil {
···
331
332
err = db.DeleteSpindle(
333
tx,
334
-
orm.FilterEq("owner", user.Active.Did),
335
orm.FilterEq("instance", instance),
336
)
337
if err != nil {
···
359
360
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
361
Collection: tangled.SpindleNSID,
362
-
Repo: user.Active.Did,
363
Rkey: instance,
364
})
365
if err != nil {
···
391
}
392
393
func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) {
394
-
user := s.OAuth.GetMultiAccountUser(r)
395
l := s.Logger.With("handler", "retry")
396
397
noticeId := "operation-error"
···
407
return
408
}
409
l = l.With("instance", instance)
410
-
l = l.With("user", user.Active.Did)
411
412
spindles, err := db.GetSpindles(
413
s.Db,
414
-
orm.FilterEq("owner", user.Active.Did),
415
orm.FilterEq("instance", instance),
416
)
417
if err != nil || len(spindles) != 1 {
···
420
return
421
}
422
423
-
if string(spindles[0].Owner) != user.Active.Did {
424
-
l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner)
425
s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.")
426
return
427
}
428
429
// begin verification
430
-
err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev)
431
if err != nil {
432
l.Error("verification failed", "err", err)
433
···
445
return
446
}
447
448
-
rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did)
449
if err != nil {
450
l.Error("failed to mark verified", "err", err)
451
s.Pages.Notice(w, noticeId, err.Error())
···
473
}
474
475
func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) {
476
-
user := s.OAuth.GetMultiAccountUser(r)
477
l := s.Logger.With("handler", "addMember")
478
479
instance := chi.URLParam(r, "instance")
···
483
return
484
}
485
l = l.With("instance", instance)
486
-
l = l.With("user", user.Active.Did)
487
488
spindles, err := db.GetSpindles(
489
s.Db,
490
-
orm.FilterEq("owner", user.Active.Did),
491
orm.FilterEq("instance", instance),
492
)
493
if err != nil || len(spindles) != 1 {
···
502
s.Pages.Notice(w, noticeId, defaultErr)
503
}
504
505
-
if string(spindles[0].Owner) != user.Active.Did {
506
-
l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner)
507
s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.")
508
return
509
}
···
552
553
// add member to db
554
if err = db.AddSpindleMember(tx, models.SpindleMember{
555
-
Did: syntax.DID(user.Active.Did),
556
Rkey: rkey,
557
Instance: instance,
558
Subject: memberId.DID,
···
570
571
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
572
Collection: tangled.SpindleMemberNSID,
573
-
Repo: user.Active.Did,
574
Rkey: rkey,
575
Record: &lexutil.LexiconTypeDecoder{
576
Val: &tangled.SpindleMember{
···
603
}
604
605
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
606
-
user := s.OAuth.GetMultiAccountUser(r)
607
l := s.Logger.With("handler", "removeMember")
608
609
noticeId := "operation-error"
···
619
return
620
}
621
l = l.With("instance", instance)
622
-
l = l.With("user", user.Active.Did)
623
624
spindles, err := db.GetSpindles(
625
s.Db,
626
-
orm.FilterEq("owner", user.Active.Did),
627
orm.FilterEq("instance", instance),
628
)
629
if err != nil || len(spindles) != 1 {
···
632
return
633
}
634
635
-
if string(spindles[0].Owner) != user.Active.Did {
636
-
l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner)
637
s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.")
638
return
639
}
···
668
// get the record from the DB first:
669
members, err := db.GetSpindleMembers(
670
s.Db,
671
-
orm.FilterEq("did", user.Active.Did),
672
orm.FilterEq("instance", instance),
673
orm.FilterEq("subject", memberId.DID),
674
)
···
681
// remove from db
682
if err = db.RemoveSpindleMember(
683
tx,
684
-
orm.FilterEq("did", user.Active.Did),
685
orm.FilterEq("instance", instance),
686
orm.FilterEq("subject", memberId.DID),
687
); err != nil {
···
707
// remove from pds
708
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
709
Collection: tangled.SpindleMemberNSID,
710
-
Repo: user.Active.Did,
711
Rkey: members[0].Rkey,
712
})
713
if err != nil {
···
69
}
70
71
func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) {
72
+
user := s.OAuth.GetUser(r)
73
all, err := db.GetSpindles(
74
s.Db,
75
+
orm.FilterEq("owner", user.Did),
76
)
77
if err != nil {
78
s.Logger.Error("failed to fetch spindles", "err", err)
···
91
func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) {
92
l := s.Logger.With("handler", "dashboard")
93
94
+
user := s.OAuth.GetUser(r)
95
+
l = l.With("user", user.Did)
96
97
instance := chi.URLParam(r, "instance")
98
if instance == "" {
···
103
spindles, err := db.GetSpindles(
104
s.Db,
105
orm.FilterEq("instance", instance),
106
+
orm.FilterEq("owner", user.Did),
107
orm.FilterIsNot("verified", "null"),
108
)
109
if err != nil || len(spindles) != 1 {
···
155
//
156
// if the spindle is not up yet, the user is free to retry verification at a later point
157
func (s *Spindles) register(w http.ResponseWriter, r *http.Request) {
158
+
user := s.OAuth.GetUser(r)
159
l := s.Logger.With("handler", "register")
160
161
noticeId := "register-error"
···
176
return
177
}
178
l = l.With("instance", instance)
179
+
l = l.With("user", user.Did)
180
181
tx, err := s.Db.Begin()
182
if err != nil {
···
190
}()
191
192
err = db.AddSpindle(tx, models.Spindle{
193
+
Owner: syntax.DID(user.Did),
194
Instance: instance,
195
})
196
if err != nil {
···
214
return
215
}
216
217
+
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Did, instance)
218
var exCid *string
219
if ex != nil {
220
exCid = ex.Cid
···
223
// re-announce by registering under same rkey
224
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
225
Collection: tangled.SpindleNSID,
226
+
Repo: user.Did,
227
Rkey: instance,
228
Record: &lexutil.LexiconTypeDecoder{
229
Val: &tangled.Spindle{
···
254
}
255
256
// begin verification
257
+
err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
258
if err != nil {
259
l.Error("verification failed", "err", err)
260
s.Pages.HxRefresh(w)
261
return
262
}
263
264
+
_, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did)
265
if err != nil {
266
l.Error("failed to mark verified", "err", err)
267
s.Pages.HxRefresh(w)
···
273
}
274
275
func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) {
276
+
user := s.OAuth.GetUser(r)
277
l := s.Logger.With("handler", "delete")
278
279
noticeId := "operation-error"
···
291
292
spindles, err := db.GetSpindles(
293
s.Db,
294
+
orm.FilterEq("owner", user.Did),
295
orm.FilterEq("instance", instance),
296
)
297
if err != nil || len(spindles) != 1 {
···
300
return
301
}
302
303
+
if string(spindles[0].Owner) != user.Did {
304
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
305
s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.")
306
return
307
}
···
320
// remove spindle members first
321
err = db.RemoveSpindleMember(
322
tx,
323
+
orm.FilterEq("did", user.Did),
324
orm.FilterEq("instance", instance),
325
)
326
if err != nil {
···
331
332
err = db.DeleteSpindle(
333
tx,
334
+
orm.FilterEq("owner", user.Did),
335
orm.FilterEq("instance", instance),
336
)
337
if err != nil {
···
359
360
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
361
Collection: tangled.SpindleNSID,
362
+
Repo: user.Did,
363
Rkey: instance,
364
})
365
if err != nil {
···
391
}
392
393
func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) {
394
+
user := s.OAuth.GetUser(r)
395
l := s.Logger.With("handler", "retry")
396
397
noticeId := "operation-error"
···
407
return
408
}
409
l = l.With("instance", instance)
410
+
l = l.With("user", user.Did)
411
412
spindles, err := db.GetSpindles(
413
s.Db,
414
+
orm.FilterEq("owner", user.Did),
415
orm.FilterEq("instance", instance),
416
)
417
if err != nil || len(spindles) != 1 {
···
420
return
421
}
422
423
+
if string(spindles[0].Owner) != user.Did {
424
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
425
s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.")
426
return
427
}
428
429
// begin verification
430
+
err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
431
if err != nil {
432
l.Error("verification failed", "err", err)
433
···
445
return
446
}
447
448
+
rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did)
449
if err != nil {
450
l.Error("failed to mark verified", "err", err)
451
s.Pages.Notice(w, noticeId, err.Error())
···
473
}
474
475
func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) {
476
+
user := s.OAuth.GetUser(r)
477
l := s.Logger.With("handler", "addMember")
478
479
instance := chi.URLParam(r, "instance")
···
483
return
484
}
485
l = l.With("instance", instance)
486
+
l = l.With("user", user.Did)
487
488
spindles, err := db.GetSpindles(
489
s.Db,
490
+
orm.FilterEq("owner", user.Did),
491
orm.FilterEq("instance", instance),
492
)
493
if err != nil || len(spindles) != 1 {
···
502
s.Pages.Notice(w, noticeId, defaultErr)
503
}
504
505
+
if string(spindles[0].Owner) != user.Did {
506
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
507
s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.")
508
return
509
}
···
552
553
// add member to db
554
if err = db.AddSpindleMember(tx, models.SpindleMember{
555
+
Did: syntax.DID(user.Did),
556
Rkey: rkey,
557
Instance: instance,
558
Subject: memberId.DID,
···
570
571
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
572
Collection: tangled.SpindleMemberNSID,
573
+
Repo: user.Did,
574
Rkey: rkey,
575
Record: &lexutil.LexiconTypeDecoder{
576
Val: &tangled.SpindleMember{
···
603
}
604
605
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
606
+
user := s.OAuth.GetUser(r)
607
l := s.Logger.With("handler", "removeMember")
608
609
noticeId := "operation-error"
···
619
return
620
}
621
l = l.With("instance", instance)
622
+
l = l.With("user", user.Did)
623
624
spindles, err := db.GetSpindles(
625
s.Db,
626
+
orm.FilterEq("owner", user.Did),
627
orm.FilterEq("instance", instance),
628
)
629
if err != nil || len(spindles) != 1 {
···
632
return
633
}
634
635
+
if string(spindles[0].Owner) != user.Did {
636
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
637
s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.")
638
return
639
}
···
668
// get the record from the DB first:
669
members, err := db.GetSpindleMembers(
670
s.Db,
671
+
orm.FilterEq("did", user.Did),
672
orm.FilterEq("instance", instance),
673
orm.FilterEq("subject", memberId.DID),
674
)
···
681
// remove from db
682
if err = db.RemoveSpindleMember(
683
tx,
684
+
orm.FilterEq("did", user.Did),
685
orm.FilterEq("instance", instance),
686
orm.FilterEq("subject", memberId.DID),
687
); err != nil {
···
707
// remove from pds
708
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
709
Collection: tangled.SpindleMemberNSID,
710
+
Repo: user.Did,
711
Rkey: members[0].Rkey,
712
})
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
-
}
···
+91
appview/state/favicon.go
+91
appview/state/favicon.go
···
···
1
+
package state
2
+
3
+
import (
4
+
"bytes"
5
+
"fmt"
6
+
"html/template"
7
+
"image"
8
+
"image/color"
9
+
"net/http"
10
+
11
+
"github.com/srwiley/oksvg"
12
+
"github.com/srwiley/rasterx"
13
+
"golang.org/x/image/draw"
14
+
"tangled.org/core/appview/pages"
15
+
"tangled.org/core/ico"
16
+
)
17
+
18
+
func (s *State) FaviconSvg(w http.ResponseWriter, r *http.Request) {
19
+
w.Header().Set("Content-Type", "image/svg+xml")
20
+
w.Header().Set("Cache-Control", "public, max-age=31536000") // one year
21
+
w.Header().Set("ETag", `"favicon-svg-v1"`)
22
+
23
+
if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` {
24
+
w.WriteHeader(http.StatusNotModified)
25
+
return
26
+
}
27
+
28
+
s.pages.Favicon(w)
29
+
}
30
+
31
+
func (s *State) FaviconIco(w http.ResponseWriter, r *http.Request) {
32
+
w.Header().Set("Content-Type", "image/x-icon")
33
+
w.Header().Set("Cache-Control", "public, max-age=31536000") // one year
34
+
w.Header().Set("ETag", `"favicon-ico-v1"`)
35
+
36
+
if match := r.Header.Get("If-None-Match"); match == `"favicon-ico-v1"` {
37
+
w.WriteHeader(http.StatusNotModified)
38
+
return
39
+
}
40
+
41
+
ico, err := dollyIco()
42
+
if err != nil {
43
+
s.logger.Error("failed to render ico", "err", err)
44
+
w.WriteHeader(http.StatusNotFound)
45
+
return
46
+
}
47
+
48
+
w.Write(ico)
49
+
}
50
+
51
+
func dollyIco() ([]byte, error) {
52
+
// first, get the bytes from the svg
53
+
tpl, err := template.New("dolly").
54
+
ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html")
55
+
if err != nil {
56
+
return nil, err
57
+
}
58
+
59
+
var svgData bytes.Buffer
60
+
if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", "#000000"); err != nil {
61
+
return nil, err
62
+
}
63
+
64
+
img, err := svgToImage(svgData.Bytes(), 48, 48)
65
+
if err != nil {
66
+
return nil, err
67
+
}
68
+
69
+
ico, err := ico.ImageToIco(img)
70
+
if err != nil {
71
+
return nil, err
72
+
}
73
+
74
+
return ico, nil
75
+
}
76
+
77
+
func svgToImage(svgData []byte, w, h int) (image.Image, error) {
78
+
icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData))
79
+
if err != nil {
80
+
return nil, fmt.Errorf("error parsing SVG: %v", err)
81
+
}
82
+
83
+
icon.SetTarget(0, 0, float64(w), float64(h))
84
+
rgba := image.NewRGBA(image.Rect(0, 0, w, h))
85
+
draw.Draw(rgba, rgba.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src)
86
+
scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())
87
+
raster := rasterx.NewDasher(w, h, scanner)
88
+
icon.Draw(raster, 1.0)
89
+
90
+
return rgba, nil
91
+
}
+7
-7
appview/state/follow.go
+7
-7
appview/state/follow.go
···
15
)
16
17
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
18
-
currentUser := s.oauth.GetMultiAccountUser(r)
19
20
subject := r.URL.Query().Get("subject")
21
if subject == "" {
···
29
return
30
}
31
32
-
if currentUser.Active.Did == subjectIdent.DID.String() {
33
log.Println("cant follow or unfollow yourself")
34
return
35
}
···
46
rkey := tid.TID()
47
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
48
Collection: tangled.GraphFollowNSID,
49
-
Repo: currentUser.Active.Did,
50
Rkey: rkey,
51
Record: &lexutil.LexiconTypeDecoder{
52
Val: &tangled.GraphFollow{
···
62
log.Println("created atproto record: ", resp.Uri)
63
64
follow := &models.Follow{
65
-
UserDid: currentUser.Active.Did,
66
SubjectDid: subjectIdent.DID.String(),
67
Rkey: rkey,
68
}
···
83
return
84
case http.MethodDelete:
85
// find the record in the db
86
-
follow, err := db.GetFollow(s.db, currentUser.Active.Did, subjectIdent.DID.String())
87
if err != nil {
88
log.Println("failed to get follow relationship")
89
return
···
91
92
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
93
Collection: tangled.GraphFollowNSID,
94
-
Repo: currentUser.Active.Did,
95
Rkey: follow.Rkey,
96
})
97
···
100
return
101
}
102
103
-
err = db.DeleteFollowByRkey(s.db, currentUser.Active.Did, follow.Rkey)
104
if err != nil {
105
log.Println("failed to delete follow from DB")
106
// this is not an issue, the firehose event might have already done this
···
15
)
16
17
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
18
+
currentUser := s.oauth.GetUser(r)
19
20
subject := r.URL.Query().Get("subject")
21
if subject == "" {
···
29
return
30
}
31
32
+
if currentUser.Did == subjectIdent.DID.String() {
33
log.Println("cant follow or unfollow yourself")
34
return
35
}
···
46
rkey := tid.TID()
47
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
48
Collection: tangled.GraphFollowNSID,
49
+
Repo: currentUser.Did,
50
Rkey: rkey,
51
Record: &lexutil.LexiconTypeDecoder{
52
Val: &tangled.GraphFollow{
···
62
log.Println("created atproto record: ", resp.Uri)
63
64
follow := &models.Follow{
65
+
UserDid: currentUser.Did,
66
SubjectDid: subjectIdent.DID.String(),
67
Rkey: rkey,
68
}
···
83
return
84
case http.MethodDelete:
85
// find the record in the db
86
+
follow, err := db.GetFollow(s.db, currentUser.Did, subjectIdent.DID.String())
87
if err != nil {
88
log.Println("failed to get follow relationship")
89
return
···
91
92
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
93
Collection: tangled.GraphFollowNSID,
94
+
Repo: currentUser.Did,
95
Rkey: follow.Rkey,
96
})
97
···
100
return
101
}
102
103
+
err = db.DeleteFollowByRkey(s.db, currentUser.Did, follow.Rkey)
104
if err != nil {
105
log.Println("failed to delete follow from DB")
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
+7
-57
appview/state/login.go
+7
-57
appview/state/login.go
···
5
"net/http"
6
"strings"
7
8
-
"tangled.org/core/appview/oauth"
9
"tangled.org/core/appview/pages"
10
)
11
···
16
case http.MethodGet:
17
returnURL := r.URL.Query().Get("return_url")
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
-
}
31
s.pages.Login(w, pages.LoginParams{
32
-
ReturnUrl: returnURL,
33
-
ErrorCode: errorCode,
34
-
AddAccount: addAccount,
35
-
LoggedInUser: user,
36
})
37
case http.MethodPost:
38
handle := r.FormValue("handle")
39
-
returnURL := r.FormValue("return_url")
40
-
addAccount := r.FormValue("add_account") == "true"
41
42
// when users copy their handle from bsky.app, it tends to have these characters around it:
43
//
···
61
return
62
}
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
-
68
redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle)
69
if err != nil {
70
l.Error("failed to start auth", "err", err)
···
79
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
80
l := s.logger.With("handler", "Logout")
81
82
-
currentUser := s.oauth.GetMultiAccountUser(r)
83
-
if currentUser == nil || currentUser.Active == nil {
84
-
s.pages.HxRedirect(w, "/login")
85
-
return
86
-
}
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")
118
s.pages.HxRedirect(w, "/login")
119
}
···
5
"net/http"
6
"strings"
7
8
"tangled.org/core/appview/pages"
9
)
10
···
15
case http.MethodGet:
16
returnURL := r.URL.Query().Get("return_url")
17
errorCode := r.URL.Query().Get("error")
18
s.pages.Login(w, pages.LoginParams{
19
+
ReturnUrl: returnURL,
20
+
ErrorCode: errorCode,
21
})
22
case http.MethodPost:
23
handle := r.FormValue("handle")
24
25
// when users copy their handle from bsky.app, it tends to have these characters around it:
26
//
···
44
return
45
}
46
47
redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle)
48
if err != nil {
49
l.Error("failed to start auth", "err", err)
···
58
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
59
l := s.logger.With("handler", "Logout")
60
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")
66
}
67
68
s.pages.HxRedirect(w, "/login")
69
}
+32
-32
appview/state/profile.go
+32
-32
appview/state/profile.go
···
77
return nil, fmt.Errorf("failed to get follower stats: %w", err)
78
}
79
80
-
loggedInUser := s.oauth.GetMultiAccountUser(r)
81
followStatus := models.IsNotFollowing
82
if loggedInUser != nil {
83
-
followStatus = db.GetFollowStatus(s.db, loggedInUser.Active.Did, did)
84
}
85
86
now := time.Now()
···
174
}
175
176
s.pages.ProfileOverview(w, pages.ProfileOverviewParams{
177
-
LoggedInUser: s.oauth.GetMultiAccountUser(r),
178
Card: profile,
179
Repos: pinnedRepos,
180
CollaboratingRepos: pinnedCollaboratingRepos,
···
205
}
206
207
err = s.pages.ProfileRepos(w, pages.ProfileReposParams{
208
-
LoggedInUser: s.oauth.GetMultiAccountUser(r),
209
Repos: repos,
210
Card: profile,
211
})
···
234
}
235
236
err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{
237
-
LoggedInUser: s.oauth.GetMultiAccountUser(r),
238
Repos: repos,
239
Card: profile,
240
})
···
259
}
260
261
err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{
262
-
LoggedInUser: s.oauth.GetMultiAccountUser(r),
263
Strings: strings,
264
Card: profile,
265
})
···
283
}
284
l = l.With("profileDid", profile.UserDid)
285
286
-
loggedInUser := s.oauth.GetMultiAccountUser(r)
287
params := FollowsPageParams{
288
Card: profile,
289
}
···
316
317
loggedInUserFollowing := make(map[string]struct{})
318
if loggedInUser != nil {
319
-
following, err := db.GetFollowing(s.db, loggedInUser.Active.Did)
320
if err != nil {
321
-
l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Active.Did)
322
return ¶ms, err
323
}
324
loggedInUserFollowing = make(map[string]struct{}, len(following))
···
333
followStatus := models.IsNotFollowing
334
if _, exists := loggedInUserFollowing[did]; exists {
335
followStatus = models.IsFollowing
336
-
} else if loggedInUser != nil && loggedInUser.Active.Did == did {
337
followStatus = models.IsSelf
338
}
339
···
367
}
368
369
s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{
370
-
LoggedInUser: s.oauth.GetMultiAccountUser(r),
371
Followers: followPage.Follows,
372
Card: followPage.Card,
373
})
···
381
}
382
383
s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{
384
-
LoggedInUser: s.oauth.GetMultiAccountUser(r),
385
Following: followPage.Follows,
386
Card: followPage.Card,
387
})
···
530
}
531
532
func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) {
533
-
user := s.oauth.GetMultiAccountUser(r)
534
535
err := r.ParseForm()
536
if err != nil {
···
539
return
540
}
541
542
-
profile, err := db.GetProfile(s.db, user.Active.Did)
543
if err != nil {
544
-
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
545
}
546
547
profile.Description = r.FormValue("description")
···
578
}
579
580
func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) {
581
-
user := s.oauth.GetMultiAccountUser(r)
582
583
err := r.ParseForm()
584
if err != nil {
···
587
return
588
}
589
590
-
profile, err := db.GetProfile(s.db, user.Active.Did)
591
if err != nil {
592
-
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
593
}
594
595
i := 0
···
617
}
618
619
func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) {
620
-
user := s.oauth.GetMultiAccountUser(r)
621
tx, err := s.db.BeginTx(r.Context(), nil)
622
if err != nil {
623
log.Println("failed to start transaction", err)
···
644
vanityStats = append(vanityStats, string(v.Kind))
645
}
646
647
-
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Active.Did, "self")
648
var cid *string
649
if ex != nil {
650
cid = ex.Cid
···
652
653
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
654
Collection: tangled.ActorProfileNSID,
655
-
Repo: user.Active.Did,
656
Rkey: "self",
657
Record: &lexutil.LexiconTypeDecoder{
658
Val: &tangled.ActorProfile{
···
681
682
s.notifier.UpdateProfile(r.Context(), profile)
683
684
-
s.pages.HxRedirect(w, "/"+user.Active.Did)
685
}
686
687
func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) {
688
-
user := s.oauth.GetMultiAccountUser(r)
689
690
-
profile, err := db.GetProfile(s.db, user.Active.Did)
691
if err != nil {
692
-
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
693
}
694
695
s.pages.EditBioFragment(w, pages.EditBioParams{
···
699
}
700
701
func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) {
702
-
user := s.oauth.GetMultiAccountUser(r)
703
704
-
profile, err := db.GetProfile(s.db, user.Active.Did)
705
if err != nil {
706
-
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
707
}
708
709
-
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Active.Did))
710
if err != nil {
711
-
log.Printf("getting repos for %s: %s", user.Active.Did, err)
712
}
713
714
-
collaboratingRepos, err := db.CollaboratingIn(s.db, user.Active.Did)
715
if err != nil {
716
-
log.Printf("getting collaborating repos for %s: %s", user.Active.Did, err)
717
}
718
719
allRepos := []pages.PinnedRepo{}
···
77
return nil, fmt.Errorf("failed to get follower stats: %w", err)
78
}
79
80
+
loggedInUser := s.oauth.GetUser(r)
81
followStatus := models.IsNotFollowing
82
if loggedInUser != nil {
83
+
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did)
84
}
85
86
now := time.Now()
···
174
}
175
176
s.pages.ProfileOverview(w, pages.ProfileOverviewParams{
177
+
LoggedInUser: s.oauth.GetUser(r),
178
Card: profile,
179
Repos: pinnedRepos,
180
CollaboratingRepos: pinnedCollaboratingRepos,
···
205
}
206
207
err = s.pages.ProfileRepos(w, pages.ProfileReposParams{
208
+
LoggedInUser: s.oauth.GetUser(r),
209
Repos: repos,
210
Card: profile,
211
})
···
234
}
235
236
err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{
237
+
LoggedInUser: s.oauth.GetUser(r),
238
Repos: repos,
239
Card: profile,
240
})
···
259
}
260
261
err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{
262
+
LoggedInUser: s.oauth.GetUser(r),
263
Strings: strings,
264
Card: profile,
265
})
···
283
}
284
l = l.With("profileDid", profile.UserDid)
285
286
+
loggedInUser := s.oauth.GetUser(r)
287
params := FollowsPageParams{
288
Card: profile,
289
}
···
316
317
loggedInUserFollowing := make(map[string]struct{})
318
if loggedInUser != nil {
319
+
following, err := db.GetFollowing(s.db, loggedInUser.Did)
320
if err != nil {
321
+
l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did)
322
return ¶ms, err
323
}
324
loggedInUserFollowing = make(map[string]struct{}, len(following))
···
333
followStatus := models.IsNotFollowing
334
if _, exists := loggedInUserFollowing[did]; exists {
335
followStatus = models.IsFollowing
336
+
} else if loggedInUser != nil && loggedInUser.Did == did {
337
followStatus = models.IsSelf
338
}
339
···
367
}
368
369
s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{
370
+
LoggedInUser: s.oauth.GetUser(r),
371
Followers: followPage.Follows,
372
Card: followPage.Card,
373
})
···
381
}
382
383
s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{
384
+
LoggedInUser: s.oauth.GetUser(r),
385
Following: followPage.Follows,
386
Card: followPage.Card,
387
})
···
530
}
531
532
func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) {
533
+
user := s.oauth.GetUser(r)
534
535
err := r.ParseForm()
536
if err != nil {
···
539
return
540
}
541
542
+
profile, err := db.GetProfile(s.db, user.Did)
543
if err != nil {
544
+
log.Printf("getting profile data for %s: %s", user.Did, err)
545
}
546
547
profile.Description = r.FormValue("description")
···
578
}
579
580
func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) {
581
+
user := s.oauth.GetUser(r)
582
583
err := r.ParseForm()
584
if err != nil {
···
587
return
588
}
589
590
+
profile, err := db.GetProfile(s.db, user.Did)
591
if err != nil {
592
+
log.Printf("getting profile data for %s: %s", user.Did, err)
593
}
594
595
i := 0
···
617
}
618
619
func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) {
620
+
user := s.oauth.GetUser(r)
621
tx, err := s.db.BeginTx(r.Context(), nil)
622
if err != nil {
623
log.Println("failed to start transaction", err)
···
644
vanityStats = append(vanityStats, string(v.Kind))
645
}
646
647
+
ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self")
648
var cid *string
649
if ex != nil {
650
cid = ex.Cid
···
652
653
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
654
Collection: tangled.ActorProfileNSID,
655
+
Repo: user.Did,
656
Rkey: "self",
657
Record: &lexutil.LexiconTypeDecoder{
658
Val: &tangled.ActorProfile{
···
681
682
s.notifier.UpdateProfile(r.Context(), profile)
683
684
+
s.pages.HxRedirect(w, "/"+user.Did)
685
}
686
687
func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) {
688
+
user := s.oauth.GetUser(r)
689
690
+
profile, err := db.GetProfile(s.db, user.Did)
691
if err != nil {
692
+
log.Printf("getting profile data for %s: %s", user.Did, err)
693
}
694
695
s.pages.EditBioFragment(w, pages.EditBioParams{
···
699
}
700
701
func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) {
702
+
user := s.oauth.GetUser(r)
703
704
+
profile, err := db.GetProfile(s.db, user.Did)
705
if err != nil {
706
+
log.Printf("getting profile data for %s: %s", user.Did, err)
707
}
708
709
+
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did))
710
if err != nil {
711
+
log.Printf("getting repos for %s: %s", user.Did, err)
712
}
713
714
+
collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did)
715
if err != nil {
716
+
log.Printf("getting collaborating repos for %s: %s", user.Did, err)
717
}
718
719
allRepos := []pages.PinnedRepo{}
+7
-7
appview/state/reaction.go
+7
-7
appview/state/reaction.go
···
17
)
18
19
func (s *State) React(w http.ResponseWriter, r *http.Request) {
20
-
currentUser := s.oauth.GetMultiAccountUser(r)
21
22
subject := r.URL.Query().Get("subject")
23
if subject == "" {
···
49
rkey := tid.TID()
50
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
51
Collection: tangled.FeedReactionNSID,
52
-
Repo: currentUser.Active.Did,
53
Rkey: rkey,
54
Record: &lexutil.LexiconTypeDecoder{
55
Val: &tangled.FeedReaction{
···
64
return
65
}
66
67
-
err = db.AddReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind, rkey)
68
if err != nil {
69
log.Println("failed to react", err)
70
return
···
87
88
return
89
case http.MethodDelete:
90
-
reaction, err := db.GetReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind)
91
if err != nil {
92
-
log.Println("failed to get reaction relationship for", currentUser.Active.Did, subjectUri)
93
return
94
}
95
96
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
97
Collection: tangled.FeedReactionNSID,
98
-
Repo: currentUser.Active.Did,
99
Rkey: reaction.Rkey,
100
})
101
···
104
return
105
}
106
107
-
err = db.DeleteReactionByRkey(s.db, currentUser.Active.Did, reaction.Rkey)
108
if err != nil {
109
log.Println("failed to delete reaction from DB")
110
// this is not an issue, the firehose event might have already done this
···
17
)
18
19
func (s *State) React(w http.ResponseWriter, r *http.Request) {
20
+
currentUser := s.oauth.GetUser(r)
21
22
subject := r.URL.Query().Get("subject")
23
if subject == "" {
···
49
rkey := tid.TID()
50
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
51
Collection: tangled.FeedReactionNSID,
52
+
Repo: currentUser.Did,
53
Rkey: rkey,
54
Record: &lexutil.LexiconTypeDecoder{
55
Val: &tangled.FeedReaction{
···
64
return
65
}
66
67
+
err = db.AddReaction(s.db, currentUser.Did, subjectUri, reactionKind, rkey)
68
if err != nil {
69
log.Println("failed to react", err)
70
return
···
87
88
return
89
case http.MethodDelete:
90
+
reaction, err := db.GetReaction(s.db, currentUser.Did, subjectUri, reactionKind)
91
if err != nil {
92
+
log.Println("failed to get reaction relationship for", currentUser.Did, subjectUri)
93
return
94
}
95
96
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
97
Collection: tangled.FeedReactionNSID,
98
+
Repo: currentUser.Did,
99
Rkey: reaction.Rkey,
100
})
101
···
104
return
105
}
106
107
+
err = db.DeleteReactionByRkey(s.db, currentUser.Did, reaction.Rkey)
108
if err != nil {
109
log.Println("failed to delete reaction from DB")
110
// this is not an issue, the firehose event might have already done this
+2
-7
appview/state/router.go
+2
-7
appview/state/router.go
···
32
s.pages,
33
)
34
35
-
router.Get("/favicon.svg", s.Favicon)
36
-
router.Get("/favicon.ico", s.Favicon)
37
router.Get("/pwa-manifest.json", s.PWAManifest)
38
router.Get("/robots.txt", s.RobotsTxt)
39
···
131
r.Get("/login", s.Login)
132
r.Post("/login", s.Login)
133
r.Post("/logout", s.Logout)
134
-
135
-
r.With(middleware.AuthMiddleware(s.oauth)).Route("/account", func(r chi.Router) {
136
-
r.Post("/switch", s.SwitchAccount)
137
-
r.Delete("/{did}", s.RemoveAccount)
138
-
})
139
140
r.Route("/repo", func(r chi.Router) {
141
r.Route("/new", func(r chi.Router) {
···
32
s.pages,
33
)
34
35
+
router.Get("/favicon.svg", s.FaviconSvg)
36
+
router.Get("/favicon.ico", s.FaviconIco)
37
router.Get("/pwa-manifest.json", s.PWAManifest)
38
router.Get("/robots.txt", s.RobotsTxt)
39
···
131
r.Get("/login", s.Login)
132
r.Post("/login", s.Login)
133
r.Post("/logout", s.Logout)
134
135
r.Route("/repo", func(r chi.Router) {
136
r.Route("/new", func(r chi.Router) {
+6
-6
appview/state/star.go
+6
-6
appview/state/star.go
···
16
)
17
18
func (s *State) Star(w http.ResponseWriter, r *http.Request) {
19
-
currentUser := s.oauth.GetMultiAccountUser(r)
20
21
subject := r.URL.Query().Get("subject")
22
if subject == "" {
···
42
rkey := tid.TID()
43
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
44
Collection: tangled.FeedStarNSID,
45
-
Repo: currentUser.Active.Did,
46
Rkey: rkey,
47
Record: &lexutil.LexiconTypeDecoder{
48
Val: &tangled.FeedStar{
···
57
log.Println("created atproto record: ", resp.Uri)
58
59
star := &models.Star{
60
-
Did: currentUser.Active.Did,
61
RepoAt: subjectUri,
62
Rkey: rkey,
63
}
···
84
return
85
case http.MethodDelete:
86
// find the record in the db
87
-
star, err := db.GetStar(s.db, currentUser.Active.Did, subjectUri)
88
if err != nil {
89
log.Println("failed to get star relationship")
90
return
···
92
93
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
94
Collection: tangled.FeedStarNSID,
95
-
Repo: currentUser.Active.Did,
96
Rkey: star.Rkey,
97
})
98
···
101
return
102
}
103
104
-
err = db.DeleteStarByRkey(s.db, currentUser.Active.Did, star.Rkey)
105
if err != nil {
106
log.Println("failed to delete star from DB")
107
// this is not an issue, the firehose event might have already done this
···
16
)
17
18
func (s *State) Star(w http.ResponseWriter, r *http.Request) {
19
+
currentUser := s.oauth.GetUser(r)
20
21
subject := r.URL.Query().Get("subject")
22
if subject == "" {
···
42
rkey := tid.TID()
43
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
44
Collection: tangled.FeedStarNSID,
45
+
Repo: currentUser.Did,
46
Rkey: rkey,
47
Record: &lexutil.LexiconTypeDecoder{
48
Val: &tangled.FeedStar{
···
57
log.Println("created atproto record: ", resp.Uri)
58
59
star := &models.Star{
60
+
Did: currentUser.Did,
61
RepoAt: subjectUri,
62
Rkey: rkey,
63
}
···
84
return
85
case http.MethodDelete:
86
// find the record in the db
87
+
star, err := db.GetStar(s.db, currentUser.Did, subjectUri)
88
if err != nil {
89
log.Println("failed to get star relationship")
90
return
···
92
93
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
94
Collection: tangled.FeedStarNSID,
95
+
Repo: currentUser.Did,
96
Rkey: star.Rkey,
97
})
98
···
101
return
102
}
103
104
+
err = db.DeleteStarByRkey(s.db, currentUser.Did, star.Rkey)
105
if err != nil {
106
log.Println("failed to delete star from DB")
107
// this is not an issue, the firehose event might have already done this
+22
-35
appview/state/state.go
+22
-35
appview/state/state.go
···
202
return s.db.Close()
203
}
204
205
-
func (s *State) Favicon(w http.ResponseWriter, r *http.Request) {
206
-
w.Header().Set("Content-Type", "image/svg+xml")
207
-
w.Header().Set("Cache-Control", "public, max-age=31536000") // one year
208
-
w.Header().Set("ETag", `"favicon-svg-v1"`)
209
-
210
-
if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` {
211
-
w.WriteHeader(http.StatusNotModified)
212
-
return
213
-
}
214
-
215
-
s.pages.Favicon(w)
216
-
}
217
-
218
func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) {
219
w.Header().Set("Content-Type", "text/plain")
220
w.Header().Set("Cache-Control", "public, max-age=86400") // one day
···
249
}
250
251
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
252
-
user := s.oauth.GetMultiAccountUser(r)
253
s.pages.TermsOfService(w, pages.TermsOfServiceParams{
254
LoggedInUser: user,
255
})
256
}
257
258
func (s *State) PrivacyPolicy(w http.ResponseWriter, r *http.Request) {
259
-
user := s.oauth.GetMultiAccountUser(r)
260
s.pages.PrivacyPolicy(w, pages.PrivacyPolicyParams{
261
LoggedInUser: user,
262
})
263
}
264
265
func (s *State) Brand(w http.ResponseWriter, r *http.Request) {
266
-
user := s.oauth.GetMultiAccountUser(r)
267
s.pages.Brand(w, pages.BrandParams{
268
LoggedInUser: user,
269
})
270
}
271
272
func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) {
273
-
if s.oauth.GetMultiAccountUser(r) != nil {
274
s.Timeline(w, r)
275
return
276
}
···
278
}
279
280
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
281
-
user := s.oauth.GetMultiAccountUser(r)
282
283
// TODO: set this flag based on the UI
284
filtered := false
285
286
var userDid string
287
-
if user != nil && user.Active != nil {
288
-
userDid = user.Active.Did
289
}
290
timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered)
291
if err != nil {
···
314
}
315
316
func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) {
317
-
user := s.oauth.GetMultiAccountUser(r)
318
if user == nil {
319
return
320
}
321
322
l := s.logger.With("handler", "UpgradeBanner")
323
-
l = l.With("did", user.Active.Did)
324
325
regs, err := db.GetRegistrations(
326
s.db,
327
-
orm.FilterEq("did", user.Active.Did),
328
orm.FilterEq("needs_upgrade", 1),
329
)
330
if err != nil {
···
333
334
spindles, err := db.GetSpindles(
335
s.db,
336
-
orm.FilterEq("owner", user.Active.Did),
337
orm.FilterEq("needs_upgrade", 1),
338
)
339
if err != nil {
···
447
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
448
switch r.Method {
449
case http.MethodGet:
450
-
user := s.oauth.GetMultiAccountUser(r)
451
-
knots, err := s.enforcer.GetKnotsForUser(user.Active.Did)
452
if err != nil {
453
s.pages.Notice(w, "repo", "Invalid user account.")
454
return
···
462
case http.MethodPost:
463
l := s.logger.With("handler", "NewRepo")
464
465
-
user := s.oauth.GetMultiAccountUser(r)
466
-
l = l.With("did", user.Active.Did)
467
468
// form validation
469
domain := r.FormValue("domain")
···
495
description := r.FormValue("description")
496
497
// ACL validation
498
-
ok, err := s.enforcer.E.Enforce(user.Active.Did, domain, domain, "repo:create")
499
if err != nil || !ok {
500
l.Info("unauthorized")
501
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
···
505
// Check for existing repos
506
existingRepo, err := db.GetRepo(
507
s.db,
508
-
orm.FilterEq("did", user.Active.Did),
509
orm.FilterEq("name", repoName),
510
)
511
if err == nil && existingRepo != nil {
···
517
// create atproto record for this repo
518
rkey := tid.TID()
519
repo := &models.Repo{
520
-
Did: user.Active.Did,
521
Name: repoName,
522
Knot: domain,
523
Rkey: rkey,
···
536
537
atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{
538
Collection: tangled.RepoNSID,
539
-
Repo: user.Active.Did,
540
Rkey: rkey,
541
Record: &lexutil.LexiconTypeDecoder{
542
Val: &record,
···
613
}
614
615
// acls
616
-
p, _ := securejoin.SecureJoin(user.Active.Did, repoName)
617
-
err = s.enforcer.AddRepo(user.Active.Did, domain, p)
618
if err != nil {
619
l.Error("acl setup failed", "err", err)
620
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
639
aturi = ""
640
641
s.notifier.NewRepo(r.Context(), repo)
642
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName))
643
}
644
}
645
···
202
return s.db.Close()
203
}
204
205
func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) {
206
w.Header().Set("Content-Type", "text/plain")
207
w.Header().Set("Cache-Control", "public, max-age=86400") // one day
···
236
}
237
238
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
239
+
user := s.oauth.GetUser(r)
240
s.pages.TermsOfService(w, pages.TermsOfServiceParams{
241
LoggedInUser: user,
242
})
243
}
244
245
func (s *State) PrivacyPolicy(w http.ResponseWriter, r *http.Request) {
246
+
user := s.oauth.GetUser(r)
247
s.pages.PrivacyPolicy(w, pages.PrivacyPolicyParams{
248
LoggedInUser: user,
249
})
250
}
251
252
func (s *State) Brand(w http.ResponseWriter, r *http.Request) {
253
+
user := s.oauth.GetUser(r)
254
s.pages.Brand(w, pages.BrandParams{
255
LoggedInUser: user,
256
})
257
}
258
259
func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) {
260
+
if s.oauth.GetUser(r) != nil {
261
s.Timeline(w, r)
262
return
263
}
···
265
}
266
267
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
268
+
user := s.oauth.GetUser(r)
269
270
// TODO: set this flag based on the UI
271
filtered := false
272
273
var userDid string
274
+
if user != nil {
275
+
userDid = user.Did
276
}
277
timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered)
278
if err != nil {
···
301
}
302
303
func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) {
304
+
user := s.oauth.GetUser(r)
305
if user == nil {
306
return
307
}
308
309
l := s.logger.With("handler", "UpgradeBanner")
310
+
l = l.With("did", user.Did)
311
312
regs, err := db.GetRegistrations(
313
s.db,
314
+
orm.FilterEq("did", user.Did),
315
orm.FilterEq("needs_upgrade", 1),
316
)
317
if err != nil {
···
320
321
spindles, err := db.GetSpindles(
322
s.db,
323
+
orm.FilterEq("owner", user.Did),
324
orm.FilterEq("needs_upgrade", 1),
325
)
326
if err != nil {
···
434
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
435
switch r.Method {
436
case http.MethodGet:
437
+
user := s.oauth.GetUser(r)
438
+
knots, err := s.enforcer.GetKnotsForUser(user.Did)
439
if err != nil {
440
s.pages.Notice(w, "repo", "Invalid user account.")
441
return
···
449
case http.MethodPost:
450
l := s.logger.With("handler", "NewRepo")
451
452
+
user := s.oauth.GetUser(r)
453
+
l = l.With("did", user.Did)
454
455
// form validation
456
domain := r.FormValue("domain")
···
482
description := r.FormValue("description")
483
484
// ACL validation
485
+
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
486
if err != nil || !ok {
487
l.Info("unauthorized")
488
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
···
492
// Check for existing repos
493
existingRepo, err := db.GetRepo(
494
s.db,
495
+
orm.FilterEq("did", user.Did),
496
orm.FilterEq("name", repoName),
497
)
498
if err == nil && existingRepo != nil {
···
504
// create atproto record for this repo
505
rkey := tid.TID()
506
repo := &models.Repo{
507
+
Did: user.Did,
508
Name: repoName,
509
Knot: domain,
510
Rkey: rkey,
···
523
524
atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{
525
Collection: tangled.RepoNSID,
526
+
Repo: user.Did,
527
Rkey: rkey,
528
Record: &lexutil.LexiconTypeDecoder{
529
Val: &record,
···
600
}
601
602
// acls
603
+
p, _ := securejoin.SecureJoin(user.Did, repoName)
604
+
err = s.enforcer.AddRepo(user.Did, domain, p)
605
if err != nil {
606
l.Error("acl setup failed", "err", err)
607
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
626
aturi = ""
627
628
s.notifier.NewRepo(r.Context(), repo)
629
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, repoName))
630
}
631
}
632
+19
-19
appview/strings/strings.go
+19
-19
appview/strings/strings.go
···
82
}
83
84
s.Pages.StringsTimeline(w, pages.StringTimelineParams{
85
-
LoggedInUser: s.OAuth.GetMultiAccountUser(r),
86
Strings: strings,
87
})
88
}
···
153
if err != nil {
154
l.Error("failed to get star count", "err", err)
155
}
156
-
user := s.OAuth.GetMultiAccountUser(r)
157
isStarred := false
158
if user != nil {
159
-
isStarred = db.GetStarStatus(s.Db, user.Active.Did, string.AtUri())
160
}
161
162
s.Pages.SingleString(w, pages.SingleStringParams{
···
178
func (s *Strings) edit(w http.ResponseWriter, r *http.Request) {
179
l := s.Logger.With("handler", "edit")
180
181
-
user := s.OAuth.GetMultiAccountUser(r)
182
183
id, ok := r.Context().Value("resolvedId").(identity.Identity)
184
if !ok {
···
216
first := all[0]
217
218
// verify that the logged in user owns this string
219
-
if user.Active.Did != id.DID.String() {
220
-
l.Error("unauthorized request", "expected", id.DID, "got", user.Active.Did)
221
w.WriteHeader(http.StatusUnauthorized)
222
return
223
}
···
226
case http.MethodGet:
227
// return the form with prefilled fields
228
s.Pages.PutString(w, pages.PutStringParams{
229
-
LoggedInUser: s.OAuth.GetMultiAccountUser(r),
230
Action: "edit",
231
String: first,
232
})
···
299
s.Notifier.EditString(r.Context(), &entry)
300
301
// if that went okay, redir to the string
302
-
s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+entry.Rkey)
303
}
304
305
}
306
307
func (s *Strings) create(w http.ResponseWriter, r *http.Request) {
308
l := s.Logger.With("handler", "create")
309
-
user := s.OAuth.GetMultiAccountUser(r)
310
311
switch r.Method {
312
case http.MethodGet:
313
s.Pages.PutString(w, pages.PutStringParams{
314
-
LoggedInUser: s.OAuth.GetMultiAccountUser(r),
315
Action: "new",
316
})
317
case http.MethodPost:
···
335
description := r.FormValue("description")
336
337
string := models.String{
338
-
Did: syntax.DID(user.Active.Did),
339
Rkey: tid.TID(),
340
Filename: filename,
341
Description: description,
···
353
354
resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{
355
Collection: tangled.StringNSID,
356
-
Repo: user.Active.Did,
357
Rkey: string.Rkey,
358
Record: &lexutil.LexiconTypeDecoder{
359
Val: &record,
···
375
s.Notifier.NewString(r.Context(), &string)
376
377
// successful
378
-
s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+string.Rkey)
379
}
380
}
381
382
func (s *Strings) delete(w http.ResponseWriter, r *http.Request) {
383
l := s.Logger.With("handler", "create")
384
-
user := s.OAuth.GetMultiAccountUser(r)
385
fail := func(msg string, err error) {
386
l.Error(msg, "err", err)
387
s.Pages.Notice(w, "error", msg)
···
402
return
403
}
404
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
return
408
}
409
410
if err := db.DeleteString(
411
s.Db,
412
-
orm.FilterEq("did", user.Active.Did),
413
orm.FilterEq("rkey", rkey),
414
); err != nil {
415
fail("Failed to delete string.", err)
416
return
417
}
418
419
-
s.Notifier.DeleteString(r.Context(), user.Active.Did, rkey)
420
421
-
s.Pages.HxRedirect(w, "/strings/"+user.Active.Did)
422
}
423
424
func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {
···
82
}
83
84
s.Pages.StringsTimeline(w, pages.StringTimelineParams{
85
+
LoggedInUser: s.OAuth.GetUser(r),
86
Strings: strings,
87
})
88
}
···
153
if err != nil {
154
l.Error("failed to get star count", "err", err)
155
}
156
+
user := s.OAuth.GetUser(r)
157
isStarred := false
158
if user != nil {
159
+
isStarred = db.GetStarStatus(s.Db, user.Did, string.AtUri())
160
}
161
162
s.Pages.SingleString(w, pages.SingleStringParams{
···
178
func (s *Strings) edit(w http.ResponseWriter, r *http.Request) {
179
l := s.Logger.With("handler", "edit")
180
181
+
user := s.OAuth.GetUser(r)
182
183
id, ok := r.Context().Value("resolvedId").(identity.Identity)
184
if !ok {
···
216
first := all[0]
217
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)
221
w.WriteHeader(http.StatusUnauthorized)
222
return
223
}
···
226
case http.MethodGet:
227
// return the form with prefilled fields
228
s.Pages.PutString(w, pages.PutStringParams{
229
+
LoggedInUser: s.OAuth.GetUser(r),
230
Action: "edit",
231
String: first,
232
})
···
299
s.Notifier.EditString(r.Context(), &entry)
300
301
// if that went okay, redir to the string
302
+
s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey)
303
}
304
305
}
306
307
func (s *Strings) create(w http.ResponseWriter, r *http.Request) {
308
l := s.Logger.With("handler", "create")
309
+
user := s.OAuth.GetUser(r)
310
311
switch r.Method {
312
case http.MethodGet:
313
s.Pages.PutString(w, pages.PutStringParams{
314
+
LoggedInUser: s.OAuth.GetUser(r),
315
Action: "new",
316
})
317
case http.MethodPost:
···
335
description := r.FormValue("description")
336
337
string := models.String{
338
+
Did: syntax.DID(user.Did),
339
Rkey: tid.TID(),
340
Filename: filename,
341
Description: description,
···
353
354
resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{
355
Collection: tangled.StringNSID,
356
+
Repo: user.Did,
357
Rkey: string.Rkey,
358
Record: &lexutil.LexiconTypeDecoder{
359
Val: &record,
···
375
s.Notifier.NewString(r.Context(), &string)
376
377
// successful
378
+
s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey)
379
}
380
}
381
382
func (s *Strings) delete(w http.ResponseWriter, r *http.Request) {
383
l := s.Logger.With("handler", "create")
384
+
user := s.OAuth.GetUser(r)
385
fail := func(msg string, err error) {
386
l.Error(msg, "err", err)
387
s.Pages.Notice(w, "error", msg)
···
402
return
403
}
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()))
407
return
408
}
409
410
if err := db.DeleteString(
411
s.Db,
412
+
orm.FilterEq("did", user.Did),
413
orm.FilterEq("rkey", rkey),
414
); err != nil {
415
fail("Failed to delete string.", err)
416
return
417
}
418
419
+
s.Notifier.DeleteString(r.Context(), user.Did, rkey)
420
421
+
s.Pages.HxRedirect(w, "/strings/"+user.Did)
422
}
423
424
func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {
+88
ico/ico.go
+88
ico/ico.go
···
···
1
+
package ico
2
+
3
+
import (
4
+
"bytes"
5
+
"encoding/binary"
6
+
"fmt"
7
+
"image"
8
+
"image/png"
9
+
)
10
+
11
+
type IconDir struct {
12
+
Reserved uint16 // must be 0
13
+
Type uint16 // 1 for ICO, 2 for CUR
14
+
Count uint16 // number of images
15
+
}
16
+
17
+
type IconDirEntry struct {
18
+
Width uint8 // 0 means 256
19
+
Height uint8 // 0 means 256
20
+
ColorCount uint8
21
+
Reserved uint8 // must be 0
22
+
ColorPlanes uint16 // 0 or 1
23
+
BitsPerPixel uint16
24
+
SizeInBytes uint32
25
+
Offset uint32
26
+
}
27
+
28
+
func ImageToIco(img image.Image) ([]byte, error) {
29
+
// encode image as png
30
+
var pngBuf bytes.Buffer
31
+
if err := png.Encode(&pngBuf, img); err != nil {
32
+
return nil, fmt.Errorf("failed to encode PNG: %w", err)
33
+
}
34
+
pngData := pngBuf.Bytes()
35
+
36
+
// get image dimensions
37
+
bounds := img.Bounds()
38
+
width := bounds.Dx()
39
+
height := bounds.Dy()
40
+
41
+
// prepare output buffer
42
+
var icoBuf bytes.Buffer
43
+
44
+
iconDir := IconDir{
45
+
Reserved: 0,
46
+
Type: 1, // ICO format
47
+
Count: 1, // One image
48
+
}
49
+
50
+
w := uint8(width)
51
+
h := uint8(height)
52
+
53
+
// width/height of 256 should be stored as 0
54
+
if width == 256 {
55
+
w = 0
56
+
}
57
+
if height == 256 {
58
+
h = 0
59
+
}
60
+
61
+
iconDirEntry := IconDirEntry{
62
+
Width: w,
63
+
Height: h,
64
+
ColorCount: 0, // 0 for PNG (32-bit)
65
+
Reserved: 0,
66
+
ColorPlanes: 1,
67
+
BitsPerPixel: 32, // PNG with alpha
68
+
SizeInBytes: uint32(len(pngData)),
69
+
Offset: 6 + 16, // Size of ICONDIR + ICONDIRENTRY
70
+
}
71
+
72
+
// write IconDir
73
+
if err := binary.Write(&icoBuf, binary.LittleEndian, iconDir); err != nil {
74
+
return nil, fmt.Errorf("failed to write ICONDIR: %w", err)
75
+
}
76
+
77
+
// write IconDirEntry
78
+
if err := binary.Write(&icoBuf, binary.LittleEndian, iconDirEntry); err != nil {
79
+
return nil, fmt.Errorf("failed to write ICONDIRENTRY: %w", err)
80
+
}
81
+
82
+
// write PNG data directly
83
+
if _, err := icoBuf.Write(pngData); err != nil {
84
+
return nil, fmt.Errorf("failed to write PNG data: %w", err)
85
+
}
86
+
87
+
return icoBuf.Bytes(), nil
88
+
}
+1
-3
lexicons/pulls/pull.json
+1
-3
lexicons/pulls/pull.json