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 19 "tangled.org/core/appview/session" 20 20 "tangled.org/core/appview/validator" 21 21 "tangled.org/core/idresolver" 22 + "tangled.org/core/rbac" 22 23 "tangled.org/core/tid" 23 24 ) 24 25 25 26 type Service struct { 26 27 config *config.Config 27 28 db *db.DB 29 + enforcer *rbac.Enforcer 28 30 indexer *issues_indexer.Indexer 29 31 logger *slog.Logger 30 32 notifier notify.Notifier ··· 36 38 logger *slog.Logger, 37 39 config *config.Config, 38 40 db *db.DB, 41 + enforcer *rbac.Enforcer, 39 42 notifier notify.Notifier, 40 43 idResolver *idresolver.Resolver, 41 44 indexer *issues_indexer.Indexer, ··· 44 47 return Service{ 45 48 config, 46 49 db, 50 + enforcer, 47 51 indexer, 48 52 logger, 49 53 notifier, ··· 53 57 } 54 58 55 59 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 + 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") 60 65 ) 61 66 62 67 func (s *Service) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) { ··· 64 69 sess := session.FromContext(ctx) 65 70 if sess == nil { 66 71 l.Error("user session is missing in context") 67 - return nil, ErrUnAuthorized 72 + return nil, ErrForbidden 68 73 } 69 74 authorDid := sess.Data.AccountDID 70 75 l = l.With("did", authorDid) ··· 176 181 sess := session.FromContext(ctx) 177 182 if sess == nil { 178 183 l.Error("user session is missing in context") 179 - return ErrUnAuthorized 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 180 192 } 181 - authorDid := sess.Data.AccountDID 182 - l = l.With("did", authorDid) 183 193 184 194 if err := s.validator.ValidateIssue(issue); err != nil { 185 195 l.Error("validation error", "err", err) ··· 233 243 sess := session.FromContext(ctx) 234 244 if sess == nil { 235 245 l.Error("user session is missing in context") 236 - return ErrUnAuthorized 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 237 254 } 238 - authorDid := sess.Data.AccountDID 239 - l = l.With("did", authorDid) 240 255 241 256 tx, err := s.db.BeginTx(ctx, nil) 242 257 if err != nil {
+72 -4
appview/service/issue/state.go
··· 3 3 import ( 4 4 "context" 5 5 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + "tangled.org/core/appview/db" 6 8 "tangled.org/core/appview/models" 9 + "tangled.org/core/appview/pages/repoinfo" 10 + "tangled.org/core/appview/session" 7 11 ) 8 12 9 - func (s *Service) CloseIssue(ctx context.Context, iusse *models.Issue) error { 10 - panic("unimplemented") 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 11 47 } 12 48 13 - func (s *Service) ReopenIssue(ctx context.Context, iusse *models.Issue) error { 14 - panic("unimplemented") 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 15 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 38 return issues.New( 39 39 s.oauth, 40 40 s.repoResolver, 41 + s.enforcer, 41 42 s.pages, 42 43 s.idResolver, 43 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 3 import ( 4 4 "net/http" 5 5 6 + "tangled.org/core/api/tangled" 7 + "tangled.org/core/appview/db" 6 8 "tangled.org/core/appview/models" 7 9 "tangled.org/core/appview/pages" 8 10 isvc "tangled.org/core/appview/service/issue" ··· 12 14 "tangled.org/core/log" 13 15 ) 14 16 15 - func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 17 + func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 16 18 return func(w http.ResponseWriter, r *http.Request) { 17 19 ctx := r.Context() 18 20 l := log.FromContext(ctx).With("handler", "Issue") ··· 28 30 user := session.UserFromContext(ctx) 29 31 repoinfo, err := rs.GetRepoInfo(ctx, issue.Repo, user) 30 32 if err != nil { 33 + l.Error("failed to load repo", "err", err) 31 34 return err 32 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 + 33 69 return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 34 - LoggedInUser: user, 35 - RepoInfo: *repoinfo, 36 - Issue: issue, 37 - CommentList: issue.CommentList(), 38 - // Backlinks: 70 + LoggedInUser: user, 71 + RepoInfo: *repoinfo, 72 + Issue: issue, 73 + CommentList: issue.CommentList(), 74 + Backlinks: backlinks, 39 75 OrderedReactionKinds: models.OrderedReactionKinds, 40 - // Reactions 41 - // UserReacted 42 - // LabelDefs 76 + Reactions: reactionMap, 77 + UserReacted: userReactions, 78 + LabelDefs: defs, 43 79 }) 44 80 }() 45 81 if err != nil {
+30 -3
appview/web/handler/user_repo_issues_issue_close.go
··· 1 1 package handler 2 2 3 3 import ( 4 + "errors" 5 + "fmt" 4 6 "net/http" 5 7 6 - "tangled.org/core/appview/service/issue" 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" 7 13 ) 8 14 9 - func CloseIssue(s issue.Service) http.HandlerFunc { 15 + func CloseIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 16 + noticeId := "issue-action" 10 17 return func(w http.ResponseWriter, r *http.Request) { 11 - panic("unimplemented") 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)) 12 39 } 13 40 }
+1
appview/web/handler/user_repo_issues_issue_edit.go
··· 70 70 } else { 71 71 p.Notice(w, noticeId, "Failed to edit issue.") 72 72 } 73 + return 73 74 } 74 75 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 1 package handler 2 2 3 3 import ( 4 + "errors" 5 + "fmt" 4 6 "net/http" 5 7 6 - "tangled.org/core/appview/service/issue" 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" 7 13 ) 8 14 9 - func ReopenIssue(s issue.Service) http.HandlerFunc { 15 + func ReopenIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 16 + noticeId := "issue-action" 10 17 return func(w http.ResponseWriter, r *http.Request) { 11 - panic("unimplemented") 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)) 12 39 } 13 40 }
+1
appview/web/handler/user_repo_issues_new.go
··· 68 68 } else { 69 69 p.Notice(w, noticeId, "Failed to create issue.") 70 70 } 71 + return 71 72 } 72 73 p.HxLocation(w, "/") 73 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 74 logger, 75 75 config, 76 76 db, 77 + enforcer, 77 78 notifier, 78 79 idResolver, 79 80 indexer.Issues, ··· 134 135 135 136 r.Mount("/settings", s.SettingsRouter()) 136 137 r.Mount("/strings", s.StringsRouter(mw)) 137 - r.Mount("/knots", s.KnotsRouter()) 138 - r.Mount("/spindles", s.SpindlesRouter()) 138 + r.Mount("/settings/knots", s.KnotsRouter()) 139 + r.Mount("/settings/spindles", s.SpindlesRouter()) 139 140 r.Mount("/notifications", s.NotificationsRouter(mw)) 140 141 141 142 r.Mount("/signup", s.SignupRouter()) ··· 169 170 r.Route("/issues/{issue}", func(r chi.Router) { 170 171 r.Use(middleware.ResolveIssue(db, pages)) 171 172 172 - r.Get("/", handler.Issue(issue, repo, pages)) 173 + r.Get("/", handler.Issue(issue, repo, pages, db)) 173 174 r.Get("/opengraph", i.IssueOpenGraphSummary) 174 175 175 176 r.With(auth).Delete("/", handler.IssueDelete(issue, pages)) ··· 177 178 r.With(auth).Get("/edit", handler.IssueEdit(issue, repo, pages)) 178 179 r.With(auth).Post("/edit", handler.IssueEditPost(issue, pages)) 179 180 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) 181 + r.With(auth).Post("/close", handler.CloseIssue(issue, pages)) 182 + r.With(auth).Post("/reopen", handler.ReopenIssue(issue, pages)) 185 183 186 184 r.With(auth).Post("/comment", i.NewIssueComment) 187 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