Compare changes

Choose any two refs to compare.

+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
··· 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
··· 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
··· 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
··· 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
··· 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), &registry); err != nil { 57 - return &AccountRegistry{Accounts: []AccountInfo{}} 58 - } 59 - 60 - return &registry 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
··· 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 package oauth 2 3 const ( 4 - SessionName = "appview-session-v2" 5 - AccountsName = "appview-accounts-v2" 6 - AuthReturnName = "appview-auth-return" 7 - AuthReturnURL = "return_url" 8 - AuthAddAccount = "add_account" 9 SessionHandle = "handle" 10 SessionDid = "did" 11 SessionId = "id"
··· 1 package oauth 2 3 const ( 4 + SessionName = "appview-session-v2" 5 SessionHandle = "handle" 6 SessionDid = "did" 7 SessionId = "id"
+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
··· 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
··· 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
··· 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 }
··· 28 emoji "github.com/yuin/goldmark-emoji" 29 "tangled.org/core/appview/filetree" 30 "tangled.org/core/appview/models" 31 "tangled.org/core/appview/pages/markup" 32 "tangled.org/core/crypto" 33 ) ··· 384 return "error" 385 } 386 return fp 387 }, 388 } 389 }
+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
··· 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
··· 11 <script defer src="/static/htmx-ext-ws.min.js"></script> 12 <script defer src="/static/actor-typeahead.js" type="module"></script> 13 14 <!-- preconnect to image cdn --> 15 <link rel="preconnect" href="https://avatar.tangled.sh" /> 16 <link rel="preconnect" href="https://camo.tangled.sh" />
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 76 // Create the blob view 77 blobView := NewBlobView(resp, rp.config, f, ref, filePath, r.URL.Query()) 78 79 - user := rp.oauth.GetMultiAccountUser(r) 80 81 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 82 LoggedInUser: user,
··· 76 // Create the blob view 77 blobView := NewBlobView(resp, rp.config, f, ref, filePath, r.URL.Query()) 78 79 + user := rp.oauth.GetUser(r) 80 81 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 82 LoggedInUser: user,
+1 -1
appview/repo/branches.go
··· 43 return 44 } 45 sortBranches(result.Branches) 46 - user := rp.oauth.GetMultiAccountUser(r) 47 rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 48 LoggedInUser: user, 49 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
··· 43 return 44 } 45 sortBranches(result.Branches) 46 + user := rp.oauth.GetUser(r) 47 rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 48 LoggedInUser: user, 49 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
+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
··· 51 Host: host, 52 } 53 54 - user := rp.oauth.GetMultiAccountUser(r) 55 56 // Build index response from multiple XRPC calls 57 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
··· 51 Host: host, 52 } 53 54 + user := rp.oauth.GetUser(r) 55 56 // Build index response from multiple XRPC calls 57 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
+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
··· 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
··· 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/tags.go
··· 69 danglingArtifacts = append(danglingArtifacts, a) 70 } 71 } 72 - user := rp.oauth.GetMultiAccountUser(r) 73 rp.pages.RepoTags(w, pages.RepoTagsParams{ 74 LoggedInUser: user, 75 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
··· 69 danglingArtifacts = append(danglingArtifacts, a) 70 } 71 } 72 + user := rp.oauth.GetUser(r) 73 rp.pages.RepoTags(w, pages.RepoTagsParams{ 74 LoggedInUser: user, 75 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
+1 -1
appview/repo/tree.go
··· 88 http.Redirect(w, r, redirectTo, http.StatusFound) 89 return 90 } 91 - user := rp.oauth.GetMultiAccountUser(r) 92 var breadcrumbs [][]string 93 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))}) 94 if treePath != "" {
··· 88 http.Redirect(w, r, redirectTo, http.StatusFound) 89 return 90 } 91 + user := rp.oauth.GetUser(r) 92 var breadcrumbs [][]string 93 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))}) 94 if treePath != "" {
+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
··· 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
··· 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
··· 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
··· 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
···
··· 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
··· 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
··· 15 ) 16 17 func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) { 18 - user := s.oauth.GetMultiAccountUser(r) 19 20 page := pagination.FromContext(r.Context()) 21
··· 15 ) 16 17 func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) { 18 + user := s.oauth.GetUser(r) 19 20 page := pagination.FromContext(r.Context()) 21
+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
··· 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 &params, 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 &params, 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
··· 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
··· 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
··· 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
··· 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
··· 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
···
··· 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
··· 32 }, 33 "patchBlob": { 34 "type": "blob", 35 - "accept": [ 36 - "text/x-patch" 37 - ], 38 "description": "patch content" 39 }, 40 "source": {
··· 32 }, 33 "patchBlob": { 34 "type": "blob", 35 + "accept": "text/x-patch", 36 "description": "patch content" 37 }, 38 "source": {