Monorepo for Tangled tangled.org

draft: appview: service layer #800

open opened by boltless.me targeting master from sl/uvpzuszrulvq

Obviously file naming of appview/web/handler/*.go files are directly against to go convention. Though I think flattening all handler files can significantly reduce the effort involved in file naming and structuring. We are already grouping core services by domains, and doing same for web handers is just over-complicating.

Signed-off-by: Seongmin Lee git@boltless.me

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3m5jyyj76xa22
+212 -38
Interdiff #4 #5
appview/oauth/handler.go

This file has not been changed.

appview/oauth/session.go

This file has not been changed.

+26 -11
appview/service/issue/issue.go
··· 19 "tangled.org/core/appview/session" 20 "tangled.org/core/appview/validator" 21 "tangled.org/core/idresolver" 22 "tangled.org/core/tid" 23 ) 24 25 type Service struct { 26 config *config.Config 27 db *db.DB 28 indexer *issues_indexer.Indexer 29 logger *slog.Logger 30 notifier notify.Notifier ··· 36 logger *slog.Logger, 37 config *config.Config, 38 db *db.DB, 39 notifier notify.Notifier, 40 idResolver *idresolver.Resolver, 41 indexer *issues_indexer.Indexer, ··· 44 return Service{ 45 config, 46 db, 47 indexer, 48 logger, 49 notifier, ··· 53 } 54 55 var ( 56 - ErrUnAuthorized = errors.New("unauthorized operation") 57 - ErrDatabaseFail = errors.New("db op fail") 58 - ErrPDSFail = errors.New("pds op fail") 59 - ErrValidationFail = errors.New("issue validation fail") 60 ) 61 62 func (s *Service) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) { ··· 64 sess := session.FromContext(ctx) 65 if sess == nil { 66 l.Error("user session is missing in context") 67 - return nil, ErrUnAuthorized 68 } 69 authorDid := sess.Data.AccountDID 70 l = l.With("did", authorDid) ··· 176 sess := session.FromContext(ctx) 177 if sess == nil { 178 l.Error("user session is missing in context") 179 - return ErrUnAuthorized 180 } 181 - authorDid := sess.Data.AccountDID 182 - l = l.With("did", authorDid) 183 184 if err := s.validator.ValidateIssue(issue); err != nil { 185 l.Error("validation error", "err", err) ··· 233 sess := session.FromContext(ctx) 234 if sess == nil { 235 l.Error("user session is missing in context") 236 - return ErrUnAuthorized 237 } 238 - authorDid := sess.Data.AccountDID 239 - l = l.With("did", authorDid) 240 241 tx, err := s.db.BeginTx(ctx, nil) 242 if err != nil {
··· 19 "tangled.org/core/appview/session" 20 "tangled.org/core/appview/validator" 21 "tangled.org/core/idresolver" 22 + "tangled.org/core/rbac" 23 "tangled.org/core/tid" 24 ) 25 26 type Service struct { 27 config *config.Config 28 db *db.DB 29 + enforcer *rbac.Enforcer 30 indexer *issues_indexer.Indexer 31 logger *slog.Logger 32 notifier notify.Notifier ··· 38 logger *slog.Logger, 39 config *config.Config, 40 db *db.DB, 41 + enforcer *rbac.Enforcer, 42 notifier notify.Notifier, 43 idResolver *idresolver.Resolver, 44 indexer *issues_indexer.Indexer, ··· 47 return Service{ 48 config, 49 db, 50 + enforcer, 51 indexer, 52 logger, 53 notifier, ··· 57 } 58 59 var ( 60 + ErrUnAuthenticated = errors.New("user session missing") 61 + ErrForbidden = errors.New("unauthorized operation") 62 + ErrDatabaseFail = errors.New("db op fail") 63 + ErrPDSFail = errors.New("pds op fail") 64 + ErrValidationFail = errors.New("issue validation fail") 65 ) 66 67 func (s *Service) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) { ··· 69 sess := session.FromContext(ctx) 70 if sess == nil { 71 l.Error("user session is missing in context") 72 + return nil, ErrForbidden 73 } 74 authorDid := sess.Data.AccountDID 75 l = l.With("did", authorDid) ··· 181 sess := session.FromContext(ctx) 182 if sess == nil { 183 l.Error("user session is missing in context") 184 + return ErrForbidden 185 + } 186 + sessDid := sess.Data.AccountDID 187 + l = l.With("did", sessDid) 188 + 189 + if sessDid != syntax.DID(issue.Did) { 190 + l.Error("only author can edit the issue") 191 + return ErrForbidden 192 } 193 194 if err := s.validator.ValidateIssue(issue); err != nil { 195 l.Error("validation error", "err", err) ··· 243 sess := session.FromContext(ctx) 244 if sess == nil { 245 l.Error("user session is missing in context") 246 + return ErrForbidden 247 + } 248 + sessDid := sess.Data.AccountDID 249 + l = l.With("did", sessDid) 250 + 251 + if sessDid != syntax.DID(issue.Did) { 252 + l.Error("only author can edit the issue") 253 + return ErrForbidden 254 } 255 256 tx, err := s.db.BeginTx(ctx, nil) 257 if err != nil {
+72 -4
appview/service/issue/state.go
··· 3 import ( 4 "context" 5 6 "tangled.org/core/appview/models" 7 ) 8 9 - func (s *Service) CloseIssue(ctx context.Context, iusse *models.Issue) error { 10 - panic("unimplemented") 11 } 12 13 - func (s *Service) ReopenIssue(ctx context.Context, iusse *models.Issue) error { 14 - panic("unimplemented") 15 }
··· 3 import ( 4 "context" 5 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + "tangled.org/core/appview/db" 8 "tangled.org/core/appview/models" 9 + "tangled.org/core/appview/pages/repoinfo" 10 + "tangled.org/core/appview/session" 11 ) 12 13 + func (s *Service) CloseIssue(ctx context.Context, issue *models.Issue) error { 14 + l := s.logger.With("method", "CloseIssue") 15 + sess := session.FromContext(ctx) 16 + if sess == nil { 17 + l.Error("user session is missing in context") 18 + return ErrUnAuthenticated 19 + } 20 + sessDid := sess.Data.AccountDID 21 + l = l.With("did", sessDid) 22 + 23 + // TODO: make this more granular 24 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(sessDid.String(), issue.Repo.Knot, issue.Repo.DidSlashRepo())} 25 + isRepoOwner := roles.IsOwner() 26 + isCollaborator := roles.IsCollaborator() 27 + isIssueOwner := sessDid == syntax.DID(issue.Did) 28 + if !(isRepoOwner || isCollaborator || isIssueOwner) { 29 + l.Error("user is not authorized") 30 + return ErrForbidden 31 + } 32 + 33 + err := db.CloseIssues( 34 + s.db, 35 + db.FilterEq("id", issue.Id), 36 + ) 37 + if err != nil { 38 + l.Error("db.CloseIssues failed", "err", err) 39 + return ErrDatabaseFail 40 + } 41 + 42 + // change the issue state (this will pass down to the notifiers) 43 + issue.Open = false 44 + 45 + s.notifier.NewIssueState(ctx, sessDid, issue) 46 + return nil 47 } 48 49 + func (s *Service) ReopenIssue(ctx context.Context, issue *models.Issue) error { 50 + l := s.logger.With("method", "ReopenIssue") 51 + sess := session.FromContext(ctx) 52 + if sess == nil { 53 + l.Error("user session is missing in context") 54 + return ErrUnAuthenticated 55 + } 56 + sessDid := sess.Data.AccountDID 57 + l = l.With("did", sessDid) 58 + 59 + // TODO: make this more granular 60 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(sessDid.String(), issue.Repo.Knot, issue.Repo.DidSlashRepo())} 61 + isRepoOwner := roles.IsOwner() 62 + isCollaborator := roles.IsCollaborator() 63 + isIssueOwner := sessDid == syntax.DID(issue.Did) 64 + if !(isRepoOwner || isCollaborator || isIssueOwner) { 65 + l.Error("user is not authorized") 66 + return ErrForbidden 67 + } 68 + 69 + err := db.ReopenIssues( 70 + s.db, 71 + db.FilterEq("id", issue.Id), 72 + ) 73 + if err != nil { 74 + l.Error("db.ReopenIssues failed", "err", err) 75 + return ErrDatabaseFail 76 + } 77 + 78 + // change the issue state (this will pass down to the notifiers) 79 + issue.Open = true 80 + 81 + s.notifier.NewIssueState(ctx, sessDid, issue) 82 + return nil 83 }
appview/service/repo/repo.go

This file has not been changed.

appview/service/repo/repoinfo.go

This file has not been changed.

appview/session/context.go

This file has not been changed.

appview/session/session.go

This file has not been changed.

+1
appview/state/legacy_bridge.go
··· 38 return issues.New( 39 s.oauth, 40 s.repoResolver, 41 s.pages, 42 s.idResolver, 43 s.db,
··· 38 return issues.New( 39 s.oauth, 40 s.repoResolver, 41 + s.enforcer, 42 s.pages, 43 s.idResolver, 44 s.db,
appview/web/handler/oauth_client_metadata.go

This file has not been changed.

appview/web/handler/oauth_jwks.go

This file has not been changed.

appview/web/handler/user_repo_issues.go

This file has not been changed.

+45 -9
appview/web/handler/user_repo_issues_issue.go
··· 3 import ( 4 "net/http" 5 6 "tangled.org/core/appview/models" 7 "tangled.org/core/appview/pages" 8 isvc "tangled.org/core/appview/service/issue" ··· 12 "tangled.org/core/log" 13 ) 14 15 - func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 16 return func(w http.ResponseWriter, r *http.Request) { 17 ctx := r.Context() 18 l := log.FromContext(ctx).With("handler", "Issue") ··· 28 user := session.UserFromContext(ctx) 29 repoinfo, err := rs.GetRepoInfo(ctx, issue.Repo, user) 30 if err != nil { 31 return err 32 } 33 return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 34 - LoggedInUser: user, 35 - RepoInfo: *repoinfo, 36 - Issue: issue, 37 - CommentList: issue.CommentList(), 38 - // Backlinks: 39 OrderedReactionKinds: models.OrderedReactionKinds, 40 - // Reactions 41 - // UserReacted 42 - // LabelDefs 43 }) 44 }() 45 if err != nil {
··· 3 import ( 4 "net/http" 5 6 + "tangled.org/core/api/tangled" 7 + "tangled.org/core/appview/db" 8 "tangled.org/core/appview/models" 9 "tangled.org/core/appview/pages" 10 isvc "tangled.org/core/appview/service/issue" ··· 14 "tangled.org/core/log" 15 ) 16 17 + func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 18 return func(w http.ResponseWriter, r *http.Request) { 19 ctx := r.Context() 20 l := log.FromContext(ctx).With("handler", "Issue") ··· 30 user := session.UserFromContext(ctx) 31 repoinfo, err := rs.GetRepoInfo(ctx, issue.Repo, user) 32 if err != nil { 33 + l.Error("failed to load repo", "err", err) 34 return err 35 } 36 + 37 + reactionMap, err := db.GetReactionMap(d, 20, issue.AtUri()) 38 + if err != nil { 39 + l.Error("failed to get issue reactions", "err", err) 40 + return err 41 + } 42 + 43 + userReactions := map[models.ReactionKind]bool{} 44 + if user != nil { 45 + userReactions = db.GetReactionStatusMap(d, user.Did, issue.AtUri()) 46 + } 47 + 48 + backlinks, err := db.GetBacklinks(d, issue.AtUri()) 49 + if err != nil { 50 + l.Error("failed to fetch backlinks", "err", err) 51 + return err 52 + } 53 + 54 + labelDefs, err := db.GetLabelDefinitions( 55 + d, 56 + db.FilterIn("at_uri", issue.Repo.Labels), 57 + db.FilterContains("scope", tangled.RepoIssueNSID), 58 + ) 59 + if err != nil { 60 + l.Error("failed to fetch label defs", "err", err) 61 + return err 62 + } 63 + 64 + defs := make(map[string]*models.LabelDefinition) 65 + for _, l := range labelDefs { 66 + defs[l.AtUri().String()] = &l 67 + } 68 + 69 return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 70 + LoggedInUser: user, 71 + RepoInfo: *repoinfo, 72 + Issue: issue, 73 + CommentList: issue.CommentList(), 74 + Backlinks: backlinks, 75 OrderedReactionKinds: models.OrderedReactionKinds, 76 + Reactions: reactionMap, 77 + UserReacted: userReactions, 78 + LabelDefs: defs, 79 }) 80 }() 81 if err != nil {
+30 -3
appview/web/handler/user_repo_issues_issue_close.go
··· 1 package handler 2 3 import ( 4 "net/http" 5 6 - "tangled.org/core/appview/service/issue" 7 ) 8 9 - func CloseIssue(s issue.Service) http.HandlerFunc { 10 return func(w http.ResponseWriter, r *http.Request) { 11 - panic("unimplemented") 12 } 13 }
··· 1 package handler 2 3 import ( 4 + "errors" 5 + "fmt" 6 "net/http" 7 8 + "tangled.org/core/appview/pages" 9 + "tangled.org/core/appview/reporesolver" 10 + isvc "tangled.org/core/appview/service/issue" 11 + "tangled.org/core/appview/web/request" 12 + "tangled.org/core/log" 13 ) 14 15 + func CloseIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 16 + noticeId := "issue-action" 17 return func(w http.ResponseWriter, r *http.Request) { 18 + ctx := r.Context() 19 + l := log.FromContext(ctx).With("handler", "CloseIssue") 20 + issue, ok := request.IssueFromContext(ctx) 21 + if !ok { 22 + l.Error("malformed request, failed to get issue") 23 + p.Error503(w) 24 + return 25 + } 26 + 27 + err := is.CloseIssue(ctx, issue) 28 + if err != nil { 29 + if errors.Is(err, isvc.ErrForbidden) { 30 + http.Error(w, "forbidden", http.StatusUnauthorized) 31 + } else { 32 + p.Notice(w, noticeId, "Failed to close issue. Try again later.") 33 + } 34 + return 35 + } 36 + 37 + ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 38 + p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 39 } 40 }
+1
appview/web/handler/user_repo_issues_issue_edit.go
··· 70 } else { 71 p.Notice(w, noticeId, "Failed to edit issue.") 72 } 73 } 74 75 p.HxRefresh(w)
··· 70 } else { 71 p.Notice(w, noticeId, "Failed to edit issue.") 72 } 73 + return 74 } 75 76 p.HxRefresh(w)
appview/web/handler/user_repo_issues_issue_opengraph.go

This file has not been changed.

+30 -3
appview/web/handler/user_repo_issues_issue_reopen.go
··· 1 package handler 2 3 import ( 4 "net/http" 5 6 - "tangled.org/core/appview/service/issue" 7 ) 8 9 - func ReopenIssue(s issue.Service) http.HandlerFunc { 10 return func(w http.ResponseWriter, r *http.Request) { 11 - panic("unimplemented") 12 } 13 }
··· 1 package handler 2 3 import ( 4 + "errors" 5 + "fmt" 6 "net/http" 7 8 + "tangled.org/core/appview/pages" 9 + "tangled.org/core/appview/reporesolver" 10 + isvc "tangled.org/core/appview/service/issue" 11 + "tangled.org/core/appview/web/request" 12 + "tangled.org/core/log" 13 ) 14 15 + func ReopenIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 16 + noticeId := "issue-action" 17 return func(w http.ResponseWriter, r *http.Request) { 18 + ctx := r.Context() 19 + l := log.FromContext(ctx).With("handler", "ReopenIssue") 20 + issue, ok := request.IssueFromContext(ctx) 21 + if !ok { 22 + l.Error("malformed request, failed to get issue") 23 + p.Error503(w) 24 + return 25 + } 26 + 27 + err := is.ReopenIssue(ctx, issue) 28 + if err != nil { 29 + if errors.Is(err, isvc.ErrForbidden) { 30 + http.Error(w, "forbidden", http.StatusUnauthorized) 31 + } else { 32 + p.Notice(w, noticeId, "Failed to reopen issue. Try again later.") 33 + } 34 + return 35 + } 36 + 37 + ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 38 + p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 39 } 40 }
+1
appview/web/handler/user_repo_issues_new.go
··· 68 } else { 69 p.Notice(w, noticeId, "Failed to create issue.") 70 } 71 } 72 p.HxLocation(w, "/") 73 }
··· 68 } else { 69 p.Notice(w, noticeId, "Failed to create issue.") 70 } 71 + return 72 } 73 p.HxLocation(w, "/") 74 }
appview/web/middleware/auth.go

This file has not been changed.

appview/web/middleware/ensuredidorhandle.go

This file has not been changed.

appview/web/middleware/log.go

This file has not been changed.

appview/web/middleware/middleware.go

This file has not been changed.

appview/web/middleware/normalize.go

This file has not been changed.

appview/web/middleware/paginate.go

This file has not been changed.

appview/web/middleware/resolve.go

This file has not been changed.

appview/web/request/context.go

This file has not been changed.

+6 -8
appview/web/routes.go
··· 74 logger, 75 config, 76 db, 77 notifier, 78 idResolver, 79 indexer.Issues, ··· 134 135 r.Mount("/settings", s.SettingsRouter()) 136 r.Mount("/strings", s.StringsRouter(mw)) 137 - r.Mount("/knots", s.KnotsRouter()) 138 - r.Mount("/spindles", s.SpindlesRouter()) 139 r.Mount("/notifications", s.NotificationsRouter(mw)) 140 141 r.Mount("/signup", s.SignupRouter()) ··· 169 r.Route("/issues/{issue}", func(r chi.Router) { 170 r.Use(middleware.ResolveIssue(db, pages)) 171 172 - r.Get("/", handler.Issue(issue, repo, pages)) 173 r.Get("/opengraph", i.IssueOpenGraphSummary) 174 175 r.With(auth).Delete("/", handler.IssueDelete(issue, pages)) ··· 177 r.With(auth).Get("/edit", handler.IssueEdit(issue, repo, pages)) 178 r.With(auth).Post("/edit", handler.IssueEditPost(issue, pages)) 179 180 - // r.With(auth).Post("/close", handler.CloseIssue(issue)) 181 - // r.With(auth).Post("/reopen", handler.ReopenIssue(issue)) 182 - 183 - r.With(auth).Post("/close", i.CloseIssue) 184 - r.With(auth).Post("/reopen", i.ReopenIssue) 185 186 r.With(auth).Post("/comment", i.NewIssueComment) 187 r.With(auth).Route("/comment/{commentId}/", func(r chi.Router) {
··· 74 logger, 75 config, 76 db, 77 + enforcer, 78 notifier, 79 idResolver, 80 indexer.Issues, ··· 135 136 r.Mount("/settings", s.SettingsRouter()) 137 r.Mount("/strings", s.StringsRouter(mw)) 138 + r.Mount("/settings/knots", s.KnotsRouter()) 139 + r.Mount("/settings/spindles", s.SpindlesRouter()) 140 r.Mount("/notifications", s.NotificationsRouter(mw)) 141 142 r.Mount("/signup", s.SignupRouter()) ··· 170 r.Route("/issues/{issue}", func(r chi.Router) { 171 r.Use(middleware.ResolveIssue(db, pages)) 172 173 + r.Get("/", handler.Issue(issue, repo, pages, db)) 174 r.Get("/opengraph", i.IssueOpenGraphSummary) 175 176 r.With(auth).Delete("/", handler.IssueDelete(issue, pages)) ··· 178 r.With(auth).Get("/edit", handler.IssueEdit(issue, repo, pages)) 179 r.With(auth).Post("/edit", handler.IssueEditPost(issue, pages)) 180 181 + r.With(auth).Post("/close", handler.CloseIssue(issue, pages)) 182 + r.With(auth).Post("/reopen", handler.ReopenIssue(issue, pages)) 183 184 r.With(auth).Post("/comment", i.NewIssueComment) 185 r.With(auth).Route("/comment/{commentId}/", func(r chi.Router) {
cmd/appview/main.go

This file has not been changed.

History

14 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
appview/{service,web}: service layer
1/3 failed, 2/3 success
expand
merge conflicts detected
expand
  • appview/pages/templates/user/login.html:33
  • appview/state/profile.go:817
  • appview/pages/templates/user/login.html:31
  • appview/pages/templates/user/login.html:93
  • appview/repo/artifact.go:251
  • appview/state/profile.go:528
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
1/3 failed, 2/3 success
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
3/3 failed
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
2/3 failed, 1/3 success
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
1/3 failed, 2/3 timeout
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
1/3 failed, 1/3 timeout, 1/3 success
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
draft: appview: service layer
3/3 failed
expand
expand 0 comments
1 commit
expand
draft: appview: service layer
3/3 failed
expand
expand 0 comments