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
+1033 -342
Interdiff #1 #2
-18
appview/service/issue/context.go
··· 1 - package issue 2 - 3 - import ( 4 - "context" 5 - 6 - "tangled.org/core/appview/models" 7 - ) 8 - 9 - type ctxKey struct{} 10 - 11 - func IntoContext(ctx context.Context, repo *models.Issue) context.Context { 12 - return context.WithValue(ctx, ctxKey{}, repo) 13 - } 14 - 15 - func FromContext(ctx context.Context) (*models.Issue, bool) { 16 - repo, ok := ctx.Value(ctxKey{}).(*models.Issue) 17 - return repo, ok 18 - }
+105 -47
appview/service/issue/issue.go
··· 7 7 "time" 8 8 9 9 "github.com/bluesky-social/indigo/api/atproto" 10 - "github.com/bluesky-social/indigo/atproto/auth/oauth" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 11 lexutil "github.com/bluesky-social/indigo/lex/util" 12 12 "tangled.org/core/api/tangled" 13 13 "tangled.org/core/appview/config" 14 14 "tangled.org/core/appview/db" 15 + issues_indexer "tangled.org/core/appview/indexer/issues" 15 16 "tangled.org/core/appview/models" 16 17 "tangled.org/core/appview/notify" 17 - "tangled.org/core/appview/refresolver" 18 + "tangled.org/core/appview/pages/markup" 19 + "tangled.org/core/appview/session" 20 + "tangled.org/core/appview/validator" 21 + "tangled.org/core/idresolver" 18 22 "tangled.org/core/tid" 19 23 ) 20 24 21 - type IssueService struct { 22 - logger *slog.Logger 23 - config *config.Config 24 - db *db.DB 25 - notifier notify.Notifier 26 - refResolver *refresolver.Resolver 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 31 + idResolver *idresolver.Resolver 32 + validator *validator.Validator 27 33 } 28 34 29 35 func NewService( ··· 31 37 config *config.Config, 32 38 db *db.DB, 33 39 notifier notify.Notifier, 34 - refResolver *refresolver.Resolver, 35 - ) IssueService { 36 - return IssueService{ 37 - logger, 40 + idResolver *idresolver.Resolver, 41 + indexer *issues_indexer.Indexer, 42 + validator *validator.Validator, 43 + ) Service { 44 + return Service{ 38 45 config, 39 46 db, 47 + indexer, 48 + logger, 40 49 notifier, 41 - refResolver, 50 + idResolver, 51 + validator, 42 52 } 43 53 } 44 54 45 55 var ( 46 - ErrCtxMissing = errors.New("context values are missing") 56 + ErrUnAuthorized = errors.New("unauthorized operation") 47 57 ErrDatabaseFail = errors.New("db op fail") 48 58 ErrPDSFail = errors.New("pds op fail") 49 59 ErrValidationFail = errors.New("issue validation fail") 50 60 ) 51 61 52 - // TODO: NewIssue should return typed errors 53 - func (s *IssueService) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) { 62 + func (s *Service) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) { 54 63 l := s.logger.With("method", "NewIssue") 55 - sess, ok := fromContext(ctx) 56 - if !ok { 64 + sess := session.FromContext(ctx) 65 + if sess == nil { 57 66 l.Error("user session is missing in context") 58 - return nil, ErrCtxMissing 67 + return nil, ErrUnAuthorized 59 68 } 60 69 authorDid := sess.Data.AccountDID 61 70 l = l.With("did", authorDid) 62 71 63 - mentions, references := s.refResolver.Resolve(ctx, body) 72 + // mentions, references := s.refResolver.Resolve(ctx, body) 73 + mentions := func() []syntax.DID { 74 + rawMentions := markup.FindUserMentions(body) 75 + idents := s.idResolver.ResolveIdents(ctx, rawMentions) 76 + l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 77 + var mentions []syntax.DID 78 + for _, ident := range idents { 79 + if ident != nil && !ident.Handle.IsInvalidHandle() { 80 + mentions = append(mentions, ident.DID) 81 + } 82 + } 83 + return mentions 84 + }() 64 85 65 86 issue := models.Issue{ 66 - RepoAt: repo.RepoAt(), 67 - Rkey: tid.TID(), 68 - Title: title, 69 - Body: body, 70 - Open: true, 71 - Did: authorDid.String(), 72 - Created: time.Now(), 73 - Mentions: mentions, 74 - References: references, 75 - Repo: repo, 87 + RepoAt: repo.RepoAt(), 88 + Rkey: tid.TID(), 89 + Title: title, 90 + Body: body, 91 + Open: true, 92 + Did: authorDid.String(), 93 + Created: time.Now(), 94 + Repo: repo, 95 + } 96 + 97 + if err := s.validator.ValidateIssue(&issue); err != nil { 98 + l.Error("validation error", "err", err) 99 + return nil, ErrValidationFail 76 100 } 77 - // TODO: validate the issue 78 101 79 102 tx, err := s.db.BeginTx(ctx, nil) 80 103 if err != nil { ··· 111 134 return &issue, nil 112 135 } 113 136 114 - func (s *IssueService) EditIssue(ctx context.Context, issue *models.Issue) error { 137 + func (s *Service) GetIssues(ctx context.Context, repo *models.Repo, searchOpts models.IssueSearchOptions) ([]models.Issue, error) { 138 + l := s.logger.With("method", "EditIssue") 139 + 140 + var issues []models.Issue 141 + var err error 142 + if searchOpts.Keyword != "" { 143 + res, err := s.indexer.Search(ctx, searchOpts) 144 + if err != nil { 145 + l.Error("failed to search for issues", "err", err) 146 + return nil, err 147 + } 148 + l.Debug("searched issues with indexer", "count", len(res.Hits)) 149 + issues, err = db.GetIssues(s.db, db.FilterIn("id", res.Hits)) 150 + if err != nil { 151 + l.Error("failed to get issues", "err", err) 152 + return nil, err 153 + } 154 + } else { 155 + openInt := 0 156 + if searchOpts.IsOpen { 157 + openInt = 1 158 + } 159 + issues, err = db.GetIssuesPaginated( 160 + s.db, 161 + searchOpts.Page, 162 + db.FilterEq("repo_at", repo.RepoAt()), 163 + db.FilterEq("open", openInt), 164 + ) 165 + if err != nil { 166 + l.Error("failed to get issues", "err", err) 167 + return nil, err 168 + } 169 + } 170 + 171 + return issues, nil 172 + } 173 + 174 + func (s *Service) EditIssue(ctx context.Context, issue *models.Issue) error { 115 175 l := s.logger.With("method", "EditIssue") 116 - sess, ok := fromContext(ctx) 117 - if !ok { 176 + sess := session.FromContext(ctx) 177 + if sess == nil { 118 178 l.Error("user session is missing in context") 119 - return ErrCtxMissing 179 + return ErrUnAuthorized 120 180 } 121 181 authorDid := sess.Data.AccountDID 122 182 l = l.With("did", authorDid) 123 183 124 - // TODO: validate issue 184 + if err := s.validator.ValidateIssue(issue); err != nil { 185 + l.Error("validation error", "err", err) 186 + return ErrValidationFail 187 + } 125 188 126 189 tx, err := s.db.BeginTx(ctx, nil) 127 190 if err != nil { ··· 165 228 return nil 166 229 } 167 230 168 - func (s *IssueService) DeleteIssue(ctx context.Context, issue *models.Issue) error { 231 + func (s *Service) DeleteIssue(ctx context.Context, issue *models.Issue) error { 169 232 l := s.logger.With("method", "DeleteIssue") 170 - sess, ok := fromContext(ctx) 171 - if !ok { 172 - return ErrCtxMissing 233 + sess := session.FromContext(ctx) 234 + if sess == nil { 235 + l.Error("user session is missing in context") 236 + return ErrUnAuthorized 173 237 } 174 238 authorDid := sess.Data.AccountDID 175 239 l = l.With("did", authorDid) ··· 181 245 } 182 246 defer tx.Rollback() 183 247 184 - if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil { 248 + if err := db.DeleteIssues(tx, db.FilterEq("id", issue.Id)); err != nil { 185 249 l.Error("db.DeleteIssues failed", "err", err) 186 250 return ErrDatabaseFail 187 251 } ··· 205 269 s.notifier.DeleteIssue(ctx, issue) 206 270 return nil 207 271 } 208 - 209 - // TODO: remove this 210 - func fromContext(ctx context.Context) (*oauth.ClientSession, bool) { 211 - sess, ok := ctx.Value("sess").(*oauth.ClientSession) 212 - return sess, ok 213 - }
+2 -2
appview/service/issue/state.go
··· 6 6 "tangled.org/core/appview/models" 7 7 ) 8 8 9 - func (s *IssueService) CloseIssue(ctx context.Context, iusse *models.Issue) error { 9 + func (s *Service) CloseIssue(ctx context.Context, iusse *models.Issue) error { 10 10 panic("unimplemented") 11 11 } 12 12 13 - func (s *IssueService) ReopenIssue(ctx context.Context, iusse *models.Issue) error { 13 + func (s *Service) ReopenIssue(ctx context.Context, iusse *models.Issue) error { 14 14 panic("unimplemented") 15 15 }
-18
appview/service/owner/context.go
··· 1 - package owner 2 - 3 - import ( 4 - "context" 5 - 6 - "github.com/bluesky-social/indigo/atproto/identity" 7 - ) 8 - 9 - type ctxKey struct{} 10 - 11 - func IntoContext(ctx context.Context, id *identity.Identity) context.Context { 12 - return context.WithValue(ctx, ctxKey{}, id) 13 - } 14 - 15 - func FromContext(ctx context.Context) (*identity.Identity, bool) { 16 - repo, ok := ctx.Value(ctxKey{}).(*identity.Identity) 17 - return repo, ok 18 - }
-18
appview/service/repo/context.go
··· 1 - package repo 2 - 3 - import ( 4 - "context" 5 - 6 - "tangled.org/core/appview/models" 7 - ) 8 - 9 - type ctxKey struct{} 10 - 11 - func IntoContext(ctx context.Context, repo *models.Repo) context.Context { 12 - return context.WithValue(ctx, ctxKey{}, repo) 13 - } 14 - 15 - func FromContext(ctx context.Context) (*models.Repo, bool) { 16 - repo, ok := ctx.Value(ctxKey{}).(*models.Repo) 17 - return repo, ok 18 - }
+18 -6
appview/service/repo/repo.go
··· 16 16 "tangled.org/core/tid" 17 17 ) 18 18 19 - type RepoService struct { 19 + type Service struct { 20 20 logger *slog.Logger 21 21 config *config.Config 22 22 db *db.DB 23 23 enforcer *rbac.Enforcer 24 24 } 25 25 26 + func NewService( 27 + logger *slog.Logger, 28 + config *config.Config, 29 + db *db.DB, 30 + enforcer *rbac.Enforcer, 31 + ) Service { 32 + return Service{ 33 + logger, 34 + config, 35 + db, 36 + enforcer, 37 + } 38 + } 39 + 26 40 // NewRepo creates a repository 27 41 // It expects atproto session to be passed in `ctx` 28 - func (s *RepoService) NewRepo(ctx context.Context, name, description, knot string) error { 42 + func (s *Service) NewRepo(ctx context.Context, name, description, knot string) error { 29 43 l := s.logger.With("method", "NewRepo") 30 44 sess := fromContext(ctx) 31 45 ··· 49 63 } 50 64 defer tx.Rollback() 51 65 52 - 53 66 atpclient := sess.APIClient() 54 67 _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 55 68 Collection: tangled.RepoNSID, 56 - Repo: repo.Did, 69 + Repo: repo.Did, 57 70 }) 58 71 if err != nil { 59 72 return fmt.Errorf("atproto.RepoPutRecord: %w", err) ··· 62 75 63 76 // knotclient, err := s.oauth.ServiceClient( 64 77 // ) 65 - 66 - return nil 78 + panic("unimplemented") 67 79 } 68 80 69 81 func fromContext(ctx context.Context) oauth.ClientSession {
-12
appview/state/router.go
··· 18 18 "tangled.org/core/appview/spindles" 19 19 "tangled.org/core/appview/state/userutil" 20 20 avstrings "tangled.org/core/appview/strings" 21 - "tangled.org/core/appview/web" 22 21 "tangled.org/core/log" 23 22 ) 24 23 ··· 41 40 userRouter := s.UserRouter(&middleware) 42 41 standardRouter := s.StandardRouter(&middleware) 43 42 44 - _ = web.UserRouter( 45 - s.logger, 46 - s.config, 47 - s.db, 48 - s.idResolver, 49 - s.refResolver, 50 - s.notifier, 51 - s.oauth, 52 - s.pages, 53 - ) 54 - 55 43 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 56 44 pat := chi.URLParam(r, "*") 57 45 pathParts := strings.SplitN(pat, "/", 2)
-12
appview/web/handler/issues.go
··· 1 - package handler 2 - 3 - import ( 4 - "net/http" 5 - 6 - "tangled.org/core/appview/service/issue" 7 - ) 8 - 9 - func RepoIssues(s issue.IssueService) http.HandlerFunc { 10 - return func(w http.ResponseWriter, r *http.Request) { 11 - } 12 - }
-35
appview/web/handler/issues_issue.go
··· 1 - package handler 2 - 3 - import ( 4 - "net/http" 5 - 6 - "tangled.org/core/appview/pages" 7 - isvc "tangled.org/core/appview/service/issue" 8 - "tangled.org/core/log" 9 - ) 10 - 11 - func Issue(s isvc.IssueService) http.HandlerFunc { 12 - return func(w http.ResponseWriter, r *http.Request) { 13 - panic("unimplemented") 14 - } 15 - } 16 - 17 - func IssueDelete(s isvc.IssueService, p *pages.Pages) http.HandlerFunc { 18 - noticeId := "issue-actions-error" 19 - return func(w http.ResponseWriter, r *http.Request) { 20 - ctx := r.Context() 21 - l := log.FromContext(ctx).With("handler", "IssueDelete") 22 - issue, ok := isvc.FromContext(ctx) 23 - if !ok { 24 - l.Error("failed to get issue") 25 - // TODO: 503 error with more detailed messages 26 - p.Error503(w) 27 - return 28 - } 29 - err := s.DeleteIssue(ctx, issue) 30 - if err != nil { 31 - p.Notice(w, noticeId, "failed to delete issue") 32 - } 33 - p.HxLocation(w, "/") 34 - } 35 - }
-13
appview/web/handler/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.IssueService) http.HandlerFunc { 10 - return func(w http.ResponseWriter, r *http.Request) { 11 - panic("unimplemented") 12 - } 13 - }
-19
appview/web/handler/issues_issue_edit.go
··· 1 - package handler 2 - 3 - import ( 4 - "net/http" 5 - 6 - "tangled.org/core/appview/service/issue" 7 - ) 8 - 9 - func IssueEdit(s issue.IssueService) http.HandlerFunc { 10 - return func(w http.ResponseWriter, r *http.Request) { 11 - panic("unimplemented") 12 - } 13 - } 14 - 15 - func IssueEditPost(s issue.IssueService) http.HandlerFunc { 16 - return func(w http.ResponseWriter, r *http.Request) { 17 - panic("unimplemented") 18 - } 19 - }
-13
appview/web/handler/issues_issue_opengraph.go
··· 1 - package handler 2 - 3 - import ( 4 - "net/http" 5 - 6 - "tangled.org/core/appview/service/issue" 7 - ) 8 - 9 - func IssueOpenGraph(s issue.IssueService) http.HandlerFunc { 10 - return func(w http.ResponseWriter, r *http.Request) { 11 - panic("unimplemented") 12 - } 13 - }
-13
appview/web/handler/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.IssueService) http.HandlerFunc { 10 - return func(w http.ResponseWriter, r *http.Request) { 11 - panic("unimplemented") 12 - } 13 - }
-48
appview/web/handler/issues_new.go
··· 1 - package handler 2 - 3 - import ( 4 - "errors" 5 - "net/http" 6 - 7 - "tangled.org/core/appview/pages" 8 - isvc "tangled.org/core/appview/service/issue" 9 - "tangled.org/core/appview/service/repo" 10 - "tangled.org/core/log" 11 - ) 12 - 13 - func NewIssue(p *pages.Pages) http.HandlerFunc { 14 - return func(w http.ResponseWriter, r *http.Request) { 15 - // TODO: render page 16 - } 17 - } 18 - 19 - func NewIssuePost(is isvc.IssueService, p *pages.Pages) http.HandlerFunc { 20 - noticeId := "issues" 21 - return func(w http.ResponseWriter, r *http.Request) { 22 - ctx := r.Context() 23 - l := log.FromContext(ctx).With("handler", "NewIssuePost") 24 - repo, ok := repo.FromContext(ctx) 25 - if !ok { 26 - l.Error("failed to get repo") 27 - // TODO: 503 error with more detailed messages 28 - p.Error503(w) 29 - return 30 - } 31 - var ( 32 - title = r.FormValue("title") 33 - body = r.FormValue("body") 34 - ) 35 - 36 - _, err := is.NewIssue(ctx, repo, title, body) 37 - if err != nil { 38 - if errors.Is(err, isvc.ErrDatabaseFail) { 39 - p.Notice(w, noticeId, "Failed to create issue.") 40 - } else if errors.Is(err, isvc.ErrPDSFail) { 41 - p.Notice(w, noticeId, "Failed to create issue.") 42 - } else { 43 - p.Notice(w, noticeId, "Failed to create issue.") 44 - } 45 - } 46 - p.HxLocation(w, "/") 47 - } 48 - }
-1
appview/web/handler/repos_new.go
··· 1 - package handler
-2
appview/web/handler/repos_repo.go
··· 1 - package handler 2 -
-2
appview/web/handler/repos_repo_opengraph.go
··· 1 - package handler 2 -
+30 -14
appview/web/middleware/auth.go
··· 1 1 package middleware 2 2 3 3 import ( 4 - "context" 5 4 "fmt" 6 - "log" 7 5 "net/http" 8 6 "net/url" 9 7 10 8 "tangled.org/core/appview/oauth" 9 + "tangled.org/core/appview/session" 10 + "tangled.org/core/log" 11 11 ) 12 12 13 - func AuthMiddleware(o *oauth.OAuth) middlewareFunc { 13 + // WithSession resumes atp session from cookie, ensure it's not malformed and 14 + // pass the session through context 15 + func WithSession(o *oauth.OAuth) middlewareFunc { 14 16 return func(next http.Handler) http.Handler { 15 17 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 + atSess, err := o.ResumeSession(r) 19 + if err != nil { 20 + next.ServeHTTP(w, r) 21 + return 22 + } 23 + 24 + sess := session.New(atSess) 25 + 26 + ctx := session.IntoContext(r.Context(), sess) 27 + next.ServeHTTP(w, r.WithContext(ctx)) 28 + }) 29 + } 30 + } 31 + 32 + // AuthMiddleware ensures the request is authorized and redirect to login page 33 + // when unauthorized 34 + func AuthMiddleware() middlewareFunc { 35 + return func(next http.Handler) http.Handler { 36 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 + ctx := r.Context() 38 + l := log.FromContext(ctx) 39 + 16 40 returnURL := "/" 17 41 if u, err := url.Parse(r.Header.Get("Referer")); err == nil { 18 42 returnURL = u.RequestURI() ··· 30 54 } 31 55 } 32 56 33 - sess, err := o.ResumeSession(r) 34 - if err != nil { 35 - log.Println("failed to resume session, redirecting...", "err", err, "url", r.URL.String()) 36 - redirectFunc(w, r) 37 - return 38 - } 39 - 57 + sess := session.FromContext(ctx) 40 58 if sess == nil { 41 - log.Printf("session is nil, redirecting...") 59 + l.Debug("no session, redirecting...") 42 60 redirectFunc(w, r) 43 61 return 44 62 } 45 63 46 - // TODO: use IntoContext instead 47 - ctx := context.WithValue(r.Context(), "sess", sess) 48 - next.ServeHTTP(w, r.WithContext(ctx)) 64 + next.ServeHTTP(w, r) 49 65 }) 50 66 } 51 67 }
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/paginate.go

This file has not been changed.

+27 -21
appview/web/middleware/resolve.go
··· 1 1 package middleware 2 2 3 3 import ( 4 - "log" 4 + "context" 5 5 "net/http" 6 6 "strconv" 7 7 "strings" ··· 9 9 "github.com/go-chi/chi/v5" 10 10 "tangled.org/core/appview/db" 11 11 "tangled.org/core/appview/pages" 12 - issue_service "tangled.org/core/appview/service/issue" 13 - owner_service "tangled.org/core/appview/service/owner" 14 - repo_service "tangled.org/core/appview/service/repo" 12 + "tangled.org/core/appview/web/request" 15 13 "tangled.org/core/idresolver" 14 + "tangled.org/core/log" 16 15 ) 17 16 18 17 func ResolveIdent( ··· 21 20 ) middlewareFunc { 22 21 return func(next http.Handler) http.Handler { 23 22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 + ctx := r.Context() 24 + l := log.FromContext(ctx) 24 25 didOrHandle := chi.URLParam(r, "user") 25 26 didOrHandle = strings.TrimPrefix(didOrHandle, "@") 26 27 27 - id, err := idResolver.ResolveIdent(r.Context(), didOrHandle) 28 + id, err := idResolver.ResolveIdent(ctx, didOrHandle) 28 29 if err != nil { 29 30 // invalid did or handle 30 - log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err) 31 + l.Warn("failed to resolve did/handle", "handle", didOrHandle, "err", err) 31 32 pages.Error404(w) 32 33 return 33 34 } 34 35 35 - ctx := owner_service.IntoContext(r.Context(), id) 36 - log.Println("ident resolved") 36 + ctx = request.WithOwner(ctx, id) 37 + // TODO: reomove this later 38 + ctx = context.WithValue(ctx, "resolvedId", *id) 37 39 38 40 next.ServeHTTP(w, r.WithContext(ctx)) 39 41 }) ··· 42 44 43 45 func ResolveRepo( 44 46 e *db.DB, 45 - idResolver *idresolver.Resolver, 46 47 pages *pages.Pages, 47 48 ) middlewareFunc { 48 49 return func(next http.Handler) http.Handler { 49 50 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 51 + ctx := r.Context() 52 + l := log.FromContext(ctx) 50 53 repoName := chi.URLParam(r, "repo") 51 - repoOwner, ok := owner_service.FromContext(r.Context()) 54 + repoOwner, ok := request.OwnerFromContext(ctx) 52 55 if !ok { 53 - log.Println("malformed middleware") 56 + l.Error("malformed middleware") 54 57 w.WriteHeader(http.StatusInternalServerError) 55 58 return 56 59 } ··· 61 64 db.FilterEq("name", repoName), 62 65 ) 63 66 if err != nil { 64 - log.Println("failed to resolve repo", "err", err) 67 + l.Warn("failed to resolve repo", "err", err) 65 68 pages.ErrorKnot404(w) 66 69 return 67 70 } 68 71 69 72 // TODO: pass owner id into repository object 70 73 71 - ctx := repo_service.IntoContext(r.Context(), repo) 72 - log.Println("repo resolved") 74 + ctx = request.WithRepo(ctx, repo) 75 + // TODO: reomove this later 76 + ctx = context.WithValue(ctx, "repo", repo) 73 77 74 78 next.ServeHTTP(w, r.WithContext(ctx)) 75 79 }) ··· 78 82 79 83 func ResolveIssue( 80 84 e *db.DB, 81 - idResolver *idresolver.Resolver, 82 85 pages *pages.Pages, 83 86 ) middlewareFunc { 84 87 return func(next http.Handler) http.Handler { 85 88 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 89 + ctx := r.Context() 90 + l := log.FromContext(ctx) 86 91 issueIdStr := chi.URLParam(r, "issue") 87 92 issueId, err := strconv.Atoi(issueIdStr) 88 93 if err != nil { 89 - log.Println("failed to fully resolve issue ID", err) 94 + l.Warn("failed to fully resolve issue ID", "err", err) 90 95 pages.Error404(w) 91 96 return 92 97 } 93 - repo, ok := repo_service.FromContext(r.Context()) 98 + repo, ok := request.RepoFromContext(ctx) 94 99 if !ok { 95 - log.Println("malformed middleware") 100 + l.Error("malformed middleware") 96 101 w.WriteHeader(http.StatusInternalServerError) 97 102 return 98 103 } 99 104 100 105 issue, err := db.GetIssue(e, repo.RepoAt(), issueId) 101 106 if err != nil { 102 - log.Println("failed to resolve repo", "err", err) 107 + l.Warn("failed to resolve issue", "err", err) 103 108 pages.ErrorKnot404(w) 104 109 return 105 110 } 106 111 issue.Repo = repo 107 112 108 - ctx := issue_service.IntoContext(r.Context(), issue) 109 - log.Println("issue resolved") 113 + ctx = request.WithIssue(ctx, issue) 114 + // TODO: reomove this later 115 + ctx = context.WithValue(ctx, "issue", issue) 110 116 111 117 next.ServeHTTP(w, r.WithContext(ctx)) 112 118 })
+160 -25
appview/web/routes.go
··· 7 7 "github.com/go-chi/chi/v5" 8 8 "tangled.org/core/appview/config" 9 9 "tangled.org/core/appview/db" 10 + "tangled.org/core/appview/indexer" 11 + "tangled.org/core/appview/issues" 10 12 "tangled.org/core/appview/notify" 11 13 "tangled.org/core/appview/oauth" 12 14 "tangled.org/core/appview/pages" 13 - "tangled.org/core/appview/refresolver" 14 - "tangled.org/core/appview/service/issue" 15 + isvc "tangled.org/core/appview/service/issue" 16 + rsvc "tangled.org/core/appview/service/repo" 17 + "tangled.org/core/appview/state" 18 + "tangled.org/core/appview/validator" 15 19 "tangled.org/core/appview/web/handler" 16 20 "tangled.org/core/appview/web/middleware" 17 21 "tangled.org/core/idresolver" 22 + "tangled.org/core/log" 23 + "tangled.org/core/rbac" 18 24 ) 19 25 20 26 // Rules 21 27 // - Use single function for each endpoints (unless it doesn't make sense.) 22 28 // - Name handler files following the related path (ancestor paths can be 23 29 // trimmed.) 24 - // - Uass dependencies to each handlers, don't create structs with shared 30 + // - Pass dependencies to each handlers, don't create structs with shared 25 31 // dependencies unless it serves some domain-specific roles like 26 32 // service/issue. Same rule goes to middlewares. 27 33 28 - func UserRouter( 34 + // RouterFromState creates a web router from `state.State`. This exist to 35 + // bridge between legacy web routers under `State` and new architecture 36 + func RouterFromState(s *state.State) http.Handler { 37 + config, db, enforcer, idResolver, indexer, logger, notifier, oauth, pages, repoResolver, validator := s.Expose() 38 + i := issues.New( 39 + oauth, 40 + repoResolver, 41 + pages, 42 + idResolver, 43 + db, 44 + config, 45 + notifier, 46 + validator, 47 + indexer.Issues, 48 + log.SubLogger(logger, "issues"), 49 + ) 50 + 51 + return Router( 52 + logger, 53 + config, 54 + db, 55 + enforcer, 56 + idResolver, 57 + indexer, 58 + notifier, 59 + oauth, 60 + pages, 61 + validator, 62 + s, 63 + i, 64 + ) 65 + } 66 + 67 + func Router( 29 68 // NOTE: put base dependencies (db, idResolver, oauth etc) 30 69 logger *slog.Logger, 31 70 config *config.Config, 32 71 db *db.DB, 72 + enforcer *rbac.Enforcer, 33 73 idResolver *idresolver.Resolver, 34 - refResolver *refresolver.Resolver, 74 + indexer *indexer.Indexer, 35 75 notifier notify.Notifier, 36 76 oauth *oauth.OAuth, 37 77 pages *pages.Pages, 78 + validator *validator.Validator, 79 + // to use legacy web handlers. will be removed later 80 + s *state.State, 81 + i *issues.Issues, 38 82 ) http.Handler { 39 - r := chi.NewRouter() 40 - 41 - auth := middleware.AuthMiddleware(oauth) 42 - 43 - issue := issue.NewService( 83 + repo := rsvc.NewService( 84 + logger, 85 + config, 86 + db, 87 + enforcer, 88 + ) 89 + issue := isvc.NewService( 44 90 logger, 45 91 config, 46 92 db, 47 93 notifier, 48 - refResolver, 94 + idResolver, 95 + indexer.Issues, 96 + validator, 49 97 ) 50 98 99 + r := chi.NewRouter() 100 + 101 + mw := s.Middleware() 102 + auth := middleware.AuthMiddleware() 103 + 51 104 r.Use(middleware.WithLogger(logger)) 105 + r.Use(middleware.WithSession(oauth)) 106 + 107 + r.Use(middleware.Normalize()) 108 + 109 + r.Get("/favicon.svg", s.Favicon) 110 + r.Get("/favicon.ico", s.Favicon) 111 + r.Get("/pwa-manifest.json", s.PWAManifest) 112 + r.Get("/robots.txt", s.RobotsTxt) 113 + 114 + r.Handle("/static/*", pages.Static()) 115 + 116 + r.Get("/", s.HomeOrTimeline) 117 + r.Get("/timeline", s.Timeline) 118 + r.Get("/upgradeBanner", s.UpgradeBanner) 119 + 120 + r.Get("/terms", s.TermsOfService) 121 + r.Get("/privacy", s.PrivacyPolicy) 122 + r.Get("/brand", s.Brand) 123 + // special-case handler for serving tangled.org/core 124 + r.Get("/core", s.Core()) 125 + 126 + r.Get("/login", s.Login) 127 + r.Post("/login", s.Login) 128 + r.Post("/logout", s.Logout) 129 + 130 + r.Get("/goodfirstissues", s.GoodFirstIssues) 131 + 132 + r.With(auth).Get("/repo/new", s.NewRepo) 133 + r.With(auth).Post("/repo/new", s.NewRepo) 134 + 135 + r.With(auth).Post("/follow", s.Follow) 136 + r.With(auth).Delete("/follow", s.Follow) 137 + 138 + r.With(auth).Post("/star", s.Star) 139 + r.With(auth).Delete("/star", s.Star) 140 + 141 + r.With(auth).Post("/react", s.React) 142 + r.With(auth).Delete("/react", s.React) 143 + 144 + r.With(auth).Get("/profile/edit-bio", s.EditBioFragment) 145 + r.With(auth).Get("/profile/edit-pins", s.EditPinsFragment) 146 + r.With(auth).Post("/profile/bio", s.UpdateProfileBio) 147 + r.With(auth).Post("/profile/pins", s.UpdateProfilePins) 148 + 149 + r.Mount("/settings", s.SettingsRouter()) 150 + r.Mount("/strings", s.StringsRouter(mw)) 151 + r.Mount("/knots", s.KnotsRouter()) 152 + r.Mount("/spindles", s.SpindlesRouter()) 153 + r.Mount("/notifications", s.NotificationsRouter(mw)) 154 + 155 + r.Mount("/signup", s.SignupRouter()) 156 + r.Get("/oauth/client-metadata.json", handler.OauthClientMetadata(oauth)) 157 + r.Get("/oauth/jwks.json", handler.OauthJwks(oauth)) 158 + r.Get("/oauth/callback", oauth.Callback) 159 + 160 + // special-case handler. should replace with xrpc later 161 + r.Get("/keys/{user}", s.Keys) 52 162 53 163 r.Route("/{user}", func(r chi.Router) { 164 + r.Use(middleware.EnsureDidOrHandle(pages)) 54 165 r.Use(middleware.ResolveIdent(idResolver, pages)) 55 166 56 - // r.Get("/", Profile) 57 - // r.Get("/feed.atom", AtomFeedPage) 167 + r.Get("/", s.Profile) 168 + r.Get("/feed.atom", s.AtomFeedPage) 58 169 59 170 r.Route("/{repo}", func(r chi.Router) { 60 - r.Use(middleware.ResolveRepo(db, idResolver, pages)) 171 + r.Use(middleware.ResolveRepo(db, pages)) 172 + 173 + r.Mount("/", s.RepoRouter(mw)) 61 174 62 175 // /{user}/{repo}/issues/* 63 - r.With(middleware.Paginate).Get("/issues", handler.RepoIssues(issue)) 64 - r.With(auth).Get("/issues/new", handler.NewIssue(pages)) 176 + r.With(middleware.Paginate).Get("/issues", handler.RepoIssues(issue, repo, pages, db)) 177 + r.With(auth).Get("/issues/new", handler.NewIssue(repo, pages)) 65 178 r.With(auth).Post("/issues/new", handler.NewIssuePost(issue, pages)) 66 179 r.Route("/issues/{issue}", func(r chi.Router) { 67 - r.Use(middleware.ResolveIssue(db, idResolver, pages)) 180 + r.Use(middleware.ResolveIssue(db, pages)) 68 181 69 - r.Get("/", handler.Issue(issue)) 70 - r.Get("/opengraph", handler.IssueOpenGraph(issue)) 182 + r.Get("/", handler.Issue(issue, repo, pages)) 183 + r.Get("/opengraph", i.IssueOpenGraphSummary) 71 184 72 185 r.With(auth).Delete("/", handler.IssueDelete(issue, pages)) 73 186 74 - r.With(auth).Get("/edit", handler.IssueEdit(issue)) 75 - r.With(auth).Post("/edit", handler.IssueEditPost(issue)) 187 + r.With(auth).Get("/edit", handler.IssueEdit(issue, repo, pages)) 188 + r.With(auth).Post("/edit", handler.IssueEditPost(issue, pages)) 189 + 190 + // r.With(auth).Post("/close", handler.CloseIssue(issue)) 191 + // r.With(auth).Post("/reopen", handler.ReopenIssue(issue)) 76 192 77 - r.With(auth).Post("/close", handler.CloseIssue(issue)) 78 - r.With(auth).Post("/reopen", handler.ReopenIssue(issue)) 193 + r.With(auth).Post("/close", i.CloseIssue) 194 + r.With(auth).Post("/reopen", i.ReopenIssue) 79 195 80 - // TODO: comments 196 + r.With(auth).Post("/comment", i.NewIssueComment) 197 + r.With(auth).Route("/comment/{commentId}/", func(r chi.Router) { 198 + r.Get("/", i.IssueComment) 199 + r.Delete("/", i.DeleteIssueComment) 200 + r.Get("/edit", i.EditIssueComment) 201 + r.Post("/edit", i.EditIssueComment) 202 + r.Get("/reply", i.ReplyIssueComment) 203 + r.Get("/replyPlaceholder", i.ReplyIssueCommentPlaceholder) 204 + }) 81 205 }) 82 206 83 - // TODO: put more routes 207 + r.Mount("/pulls", s.PullsRouter(mw)) 208 + r.Mount("/pipelines", s.PipelinesRouter()) 209 + r.Mount("/labels", s.LabelsRouter()) 210 + 211 + // These routes get proxied to the knot 212 + r.Get("/info/refs", s.InfoRefs) 213 + r.Post("/git-upload-pack", s.UploadPack) 214 + r.Post("/git-receive-pack", s.ReceivePack) 84 215 }) 85 216 }) 86 217 218 + r.NotFound(func(w http.ResponseWriter, r *http.Request) { 219 + pages.Error404(w) 220 + }) 221 + 87 222 return r 88 223 }
+2 -2
appview/oauth/handler.go
··· 24 24 25 25 r.Get("/oauth/client-metadata.json", o.clientMetadata) 26 26 r.Get("/oauth/jwks.json", o.jwks) 27 - r.Get("/oauth/callback", o.callback) 27 + r.Get("/oauth/callback", o.Callback) 28 28 return r 29 29 } 30 30 ··· 50 50 } 51 51 } 52 52 53 - func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 53 + func (o *OAuth) Callback(w http.ResponseWriter, r *http.Request) { 54 54 ctx := r.Context() 55 55 l := o.Logger.With("query", r.URL.Query()) 56 56
+10
appview/oauth/session.go
··· 1 + package oauth 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 7 + ) 8 + 9 + func (o *OAuth) SaveSession2(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) { 10 + }
+81
appview/service/repo/repoinfo.go
··· 1 + package repo 2 + 3 + import ( 4 + "context" 5 + 6 + "tangled.org/core/appview/db" 7 + "tangled.org/core/appview/models" 8 + "tangled.org/core/appview/oauth" 9 + "tangled.org/core/appview/pages/repoinfo" 10 + ) 11 + 12 + // GetRepoInfo converts given `Repo` to `RepoInfo` object. 13 + // The `user` can be nil. 14 + func (s *Service) GetRepoInfo(ctx context.Context, baseRepo *models.Repo, user *oauth.User) (*repoinfo.RepoInfo, error) { 15 + var ( 16 + repoAt = baseRepo.RepoAt() 17 + isStarred = false 18 + roles = repoinfo.RolesInRepo{} 19 + ) 20 + if user != nil { 21 + isStarred = db.GetStarStatus(s.db, user.Did, repoAt) 22 + roles.Roles = s.enforcer.GetPermissionsInRepo(user.Did, baseRepo.Knot, baseRepo.DidSlashRepo()) 23 + } 24 + 25 + stats := baseRepo.RepoStats 26 + if stats == nil { 27 + starCount, err := db.GetStarCount(s.db, repoAt) 28 + if err != nil { 29 + return nil, err 30 + } 31 + issueCount, err := db.GetIssueCount(s.db, repoAt) 32 + if err != nil { 33 + return nil, err 34 + } 35 + pullCount, err := db.GetPullCount(s.db, repoAt) 36 + if err != nil { 37 + return nil, err 38 + } 39 + stats = &models.RepoStats{ 40 + StarCount: starCount, 41 + IssueCount: issueCount, 42 + PullCount: pullCount, 43 + } 44 + } 45 + 46 + var sourceRepo *models.Repo 47 + var err error 48 + if baseRepo.Source != "" { 49 + sourceRepo, err = db.GetRepoByAtUri(s.db, baseRepo.Source) 50 + if err != nil { 51 + return nil, err 52 + } 53 + } 54 + 55 + repoInfo := &repoinfo.RepoInfo{ 56 + // ok this is basically a models.Repo 57 + OwnerDid: baseRepo.Did, 58 + OwnerHandle: "", // TODO: shouldn't use 59 + Name: baseRepo.Name, 60 + Rkey: baseRepo.Rkey, 61 + Description: baseRepo.Description, 62 + Website: baseRepo.Website, 63 + Topics: baseRepo.Topics, 64 + Knot: baseRepo.Knot, 65 + Spindle: baseRepo.Spindle, 66 + Stats: *stats, 67 + 68 + // fork repo upstream 69 + Source: sourceRepo, 70 + 71 + // repo path (context) 72 + CurrentDir: "", 73 + Ref: "", 74 + 75 + // info related to the session 76 + IsStarred: isStarred, 77 + Roles: roles, 78 + } 79 + 80 + return repoInfo, nil 81 + }
+29
appview/session/context.go
··· 1 + package session 2 + 3 + import ( 4 + "context" 5 + 6 + toauth "tangled.org/core/appview/oauth" 7 + ) 8 + 9 + type ctxKey struct{} 10 + 11 + func IntoContext(ctx context.Context, sess Session) context.Context { 12 + return context.WithValue(ctx, ctxKey{}, &sess) 13 + } 14 + 15 + func FromContext(ctx context.Context) *Session { 16 + sess, ok := ctx.Value(ctxKey{}).(*Session) 17 + if !ok { 18 + return nil 19 + } 20 + return sess 21 + } 22 + 23 + func UserFromContext(ctx context.Context) *toauth.User { 24 + sess := FromContext(ctx) 25 + if sess == nil { 26 + return nil 27 + } 28 + return sess.User() 29 + }
+24
appview/session/session.go
··· 1 + package session 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 5 + toauth "tangled.org/core/appview/oauth" 6 + ) 7 + 8 + // Session is a lightweight wrapper over indigo-oauth ClientSession 9 + type Session struct { 10 + *oauth.ClientSession 11 + } 12 + 13 + func New(atSess *oauth.ClientSession) Session { 14 + return Session{ 15 + atSess, 16 + } 17 + } 18 + 19 + func (s *Session) User() *toauth.User { 20 + return &toauth.User{ 21 + Did: string(s.Data.AccountDID), 22 + Pds: s.Data.HostURL, 23 + } 24 + }
+47
appview/state/legacy_bridge.go
··· 1 + package state 2 + 3 + import ( 4 + "log/slog" 5 + 6 + "tangled.org/core/appview/config" 7 + "tangled.org/core/appview/db" 8 + "tangled.org/core/appview/indexer" 9 + "tangled.org/core/appview/middleware" 10 + "tangled.org/core/appview/notify" 11 + "tangled.org/core/appview/oauth" 12 + "tangled.org/core/appview/pages" 13 + "tangled.org/core/appview/reporesolver" 14 + "tangled.org/core/appview/validator" 15 + "tangled.org/core/idresolver" 16 + "tangled.org/core/rbac" 17 + ) 18 + 19 + // Expose exposes private fields in `State`. This is used to bridge between 20 + // legacy web routers and new architecture 21 + func (s *State) Expose() ( 22 + *config.Config, 23 + *db.DB, 24 + *rbac.Enforcer, 25 + *idresolver.Resolver, 26 + *indexer.Indexer, 27 + *slog.Logger, 28 + notify.Notifier, 29 + *oauth.OAuth, 30 + *pages.Pages, 31 + *reporesolver.RepoResolver, 32 + *validator.Validator, 33 + ) { 34 + return s.config, s.db, s.enforcer, s.idResolver, s.indexer, s.logger, s.notifier, s.oauth, s.pages, s.repoResolver, s.validator 35 + } 36 + 37 + func (s *State) Middleware() *middleware.Middleware { 38 + mw := middleware.New( 39 + s.oauth, 40 + s.db, 41 + s.enforcer, 42 + s.repoResolver, 43 + s.idResolver, 44 + s.pages, 45 + ) 46 + return &mw 47 + }
+23
appview/web/handler/oauth_client_metadata.go
··· 1 + package handler 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + 7 + "tangled.org/core/appview/oauth" 8 + ) 9 + 10 + func OauthClientMetadata(o *oauth.OAuth) http.HandlerFunc { 11 + return func(w http.ResponseWriter, r *http.Request) { 12 + doc := o.ClientApp.Config.ClientMetadata() 13 + doc.JWKSURI = &o.JwksUri 14 + doc.ClientName = &o.ClientName 15 + doc.ClientURI = &o.ClientUri 16 + 17 + w.Header().Set("Content-Type", "application/json") 18 + if err := json.NewEncoder(w).Encode(doc); err != nil { 19 + http.Error(w, err.Error(), http.StatusInternalServerError) 20 + return 21 + } 22 + } 23 + }
+19
appview/web/handler/oauth_jwks.go
··· 1 + package handler 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + 7 + "tangled.org/core/appview/oauth" 8 + ) 9 + 10 + func OauthJwks(o *oauth.OAuth) http.HandlerFunc { 11 + return func(w http.ResponseWriter, r *http.Request) { 12 + w.Header().Set("Content-Type", "application/json") 13 + body := o.ClientApp.Config.PublicJWKS() 14 + if err := json.NewEncoder(w).Encode(body); err != nil { 15 + http.Error(w, err.Error(), http.StatusInternalServerError) 16 + return 17 + } 18 + } 19 + }
+80
appview/web/handler/user_repo_issues.go
··· 1 + package handler 2 + 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 + "tangled.org/core/appview/pagination" 11 + isvc "tangled.org/core/appview/service/issue" 12 + rsvc "tangled.org/core/appview/service/repo" 13 + "tangled.org/core/appview/session" 14 + "tangled.org/core/appview/web/request" 15 + "tangled.org/core/log" 16 + ) 17 + 18 + func RepoIssues(is isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 19 + return func(w http.ResponseWriter, r *http.Request) { 20 + ctx := r.Context() 21 + l := log.FromContext(ctx).With("handler", "RepoIssues") 22 + repo, ok := request.RepoFromContext(ctx) 23 + if !ok { 24 + l.Error("malformed request") 25 + p.Error503(w) 26 + return 27 + } 28 + 29 + query := r.URL.Query() 30 + searchOpts := models.IssueSearchOptions{ 31 + RepoAt: repo.RepoAt().String(), 32 + Keyword: query.Get("q"), 33 + IsOpen: query.Get("state") != "closed", 34 + Page: pagination.FromContext(ctx), 35 + } 36 + 37 + issues, err := is.GetIssues(ctx, repo, searchOpts) 38 + if err != nil { 39 + l.Error("failed to get issues") 40 + p.Error503(w) 41 + return 42 + } 43 + 44 + // render page 45 + err = func() error { 46 + user := session.UserFromContext(ctx) 47 + repoinfo, err := rs.GetRepoInfo(ctx, repo, user) 48 + if err != nil { 49 + return err 50 + } 51 + labelDefs, err := db.GetLabelDefinitions( 52 + d, 53 + db.FilterIn("at_uri", repo.Labels), 54 + db.FilterContains("scope", tangled.RepoIssueNSID), 55 + ) 56 + if err != nil { 57 + return err 58 + } 59 + defs := make(map[string]*models.LabelDefinition) 60 + for _, l := range labelDefs { 61 + defs[l.AtUri().String()] = &l 62 + } 63 + return p.RepoIssues(w, pages.RepoIssuesParams{ 64 + LoggedInUser: user, 65 + RepoInfo: *repoinfo, 66 + 67 + Issues: issues, 68 + LabelDefs: defs, 69 + FilteringByOpen: searchOpts.IsOpen, 70 + FilterQuery: searchOpts.Keyword, 71 + Page: searchOpts.Page, 72 + }) 73 + }() 74 + if err != nil { 75 + l.Error("failed to render", "err", err) 76 + p.Error503(w) 77 + return 78 + } 79 + } 80 + }
+65
appview/web/handler/user_repo_issues_issue.go
··· 1 + package handler 2 + 3 + import ( 4 + "net/http" 5 + 6 + "tangled.org/core/appview/pages" 7 + isvc "tangled.org/core/appview/service/issue" 8 + rsvc "tangled.org/core/appview/service/repo" 9 + "tangled.org/core/appview/session" 10 + "tangled.org/core/appview/web/request" 11 + "tangled.org/core/log" 12 + ) 13 + 14 + func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 15 + return func(w http.ResponseWriter, r *http.Request) { 16 + ctx := r.Context() 17 + l := log.FromContext(ctx).With("handler", "Issue") 18 + issue, ok := request.IssueFromContext(ctx) 19 + if !ok { 20 + l.Error("malformed request, failed to get issue") 21 + p.Error503(w) 22 + return 23 + } 24 + 25 + // render 26 + err := func() error { 27 + user := session.UserFromContext(ctx) 28 + repoinfo, err := rs.GetRepoInfo(ctx, issue.Repo, user) 29 + if err != nil { 30 + return err 31 + } 32 + return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 33 + LoggedInUser: user, 34 + RepoInfo: *repoinfo, 35 + Issue: issue, 36 + }) 37 + }() 38 + if err != nil { 39 + l.Error("failed to render", "err", err) 40 + p.Error503(w) 41 + return 42 + } 43 + } 44 + } 45 + 46 + func IssueDelete(s isvc.Service, p *pages.Pages) http.HandlerFunc { 47 + noticeId := "issue-actions-error" 48 + return func(w http.ResponseWriter, r *http.Request) { 49 + ctx := r.Context() 50 + l := log.FromContext(ctx).With("handler", "IssueDelete") 51 + issue, ok := request.IssueFromContext(ctx) 52 + if !ok { 53 + l.Error("failed to get issue") 54 + // TODO: 503 error with more detailed messages 55 + p.Error503(w) 56 + return 57 + } 58 + err := s.DeleteIssue(ctx, issue) 59 + if err != nil { 60 + p.Notice(w, noticeId, "failed to delete issue") 61 + return 62 + } 63 + p.HxLocation(w, "/") 64 + } 65 + }
+13
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 + }
+77
appview/web/handler/user_repo_issues_issue_edit.go
··· 1 + package handler 2 + 3 + import ( 4 + "errors" 5 + "net/http" 6 + 7 + "tangled.org/core/appview/pages" 8 + isvc "tangled.org/core/appview/service/issue" 9 + rsvc "tangled.org/core/appview/service/repo" 10 + "tangled.org/core/appview/session" 11 + "tangled.org/core/appview/web/request" 12 + "tangled.org/core/log" 13 + ) 14 + 15 + func IssueEdit(is 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", "IssueEdit") 19 + issue, ok := request.IssueFromContext(ctx) 20 + if !ok { 21 + l.Error("malformed request, failed to get issue") 22 + p.Error503(w) 23 + return 24 + } 25 + 26 + // render 27 + err := func() error { 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.EditIssueFragment(w, pages.EditIssueParams{ 34 + LoggedInUser: user, 35 + RepoInfo: *repoinfo, 36 + 37 + Issue: issue, 38 + }) 39 + }() 40 + if err != nil { 41 + l.Error("failed to render", "err", err) 42 + p.Error503(w) 43 + return 44 + } 45 + } 46 + } 47 + 48 + func IssueEditPost(is isvc.Service, p *pages.Pages) http.HandlerFunc { 49 + noticeId := "issues" 50 + return func(w http.ResponseWriter, r *http.Request) { 51 + ctx := r.Context() 52 + l := log.FromContext(ctx).With("handler", "IssueEdit") 53 + issue, ok := request.IssueFromContext(ctx) 54 + if !ok { 55 + l.Error("malformed request, failed to get issue") 56 + p.Error503(w) 57 + return 58 + } 59 + 60 + newIssue := *issue 61 + newIssue.Title = r.FormValue("title") 62 + newIssue.Body = r.FormValue("body") 63 + 64 + err := is.EditIssue(ctx, &newIssue) 65 + if err != nil { 66 + if errors.Is(err, isvc.ErrDatabaseFail) { 67 + p.Notice(w, noticeId, "Failed to edit issue.") 68 + } else if errors.Is(err, isvc.ErrPDSFail) { 69 + p.Notice(w, noticeId, "Failed to edit issue.") 70 + } else { 71 + p.Notice(w, noticeId, "Failed to edit issue.") 72 + } 73 + } 74 + 75 + p.HxRefresh(w) 76 + } 77 + }
+13
appview/web/handler/user_repo_issues_issue_opengraph.go
··· 1 + package handler 2 + 3 + import ( 4 + "net/http" 5 + 6 + "tangled.org/core/appview/service/issue" 7 + ) 8 + 9 + func IssueOpenGraph(s issue.Service) http.HandlerFunc { 10 + return func(w http.ResponseWriter, r *http.Request) { 11 + panic("unimplemented") 12 + } 13 + }
+13
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 + }
+74
appview/web/handler/user_repo_issues_new.go
··· 1 + package handler 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "net/http" 7 + 8 + "tangled.org/core/appview/pages" 9 + isvc "tangled.org/core/appview/service/issue" 10 + rsvc "tangled.org/core/appview/service/repo" 11 + "tangled.org/core/appview/session" 12 + "tangled.org/core/appview/web/request" 13 + "tangled.org/core/log" 14 + ) 15 + 16 + func NewIssue(rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 17 + return func(w http.ResponseWriter, r *http.Request) { 18 + ctx := r.Context() 19 + l := log.FromContext(ctx).With("handler", "NewIssue") 20 + 21 + // render 22 + err := func() error { 23 + user := session.UserFromContext(ctx) 24 + repo, ok := request.RepoFromContext(ctx) 25 + if !ok { 26 + return fmt.Errorf("malformed request") 27 + } 28 + repoinfo, err := rs.GetRepoInfo(ctx, repo, user) 29 + if err != nil { 30 + return err 31 + } 32 + return p.RepoNewIssue(w, pages.RepoNewIssueParams{ 33 + LoggedInUser: user, 34 + RepoInfo: *repoinfo, 35 + }) 36 + }() 37 + if err != nil { 38 + l.Error("failed to render", "err", err) 39 + p.Error503(w) 40 + return 41 + } 42 + } 43 + } 44 + 45 + func NewIssuePost(is isvc.Service, p *pages.Pages) http.HandlerFunc { 46 + noticeId := "issues" 47 + return func(w http.ResponseWriter, r *http.Request) { 48 + ctx := r.Context() 49 + l := log.FromContext(ctx).With("handler", "NewIssuePost") 50 + repo, ok := request.RepoFromContext(ctx) 51 + if !ok { 52 + l.Error("malformed request, failed to get repo") 53 + // TODO: 503 error with more detailed messages 54 + p.Error503(w) 55 + return 56 + } 57 + var ( 58 + title = r.FormValue("title") 59 + body = r.FormValue("body") 60 + ) 61 + 62 + _, err := is.NewIssue(ctx, repo, title, body) 63 + if err != nil { 64 + if errors.Is(err, isvc.ErrDatabaseFail) { 65 + p.Notice(w, noticeId, "Failed to create issue.") 66 + } else if errors.Is(err, isvc.ErrPDSFail) { 67 + p.Notice(w, noticeId, "Failed to create issue.") 68 + } else { 69 + p.Notice(w, noticeId, "Failed to create issue.") 70 + } 71 + } 72 + p.HxLocation(w, "/") 73 + } 74 + }
+30
appview/web/middleware/ensuredidorhandle.go
··· 1 + package middleware 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/go-chi/chi/v5" 7 + "tangled.org/core/appview/pages" 8 + "tangled.org/core/appview/state/userutil" 9 + ) 10 + 11 + // EnsureDidOrHandle ensures the "user" url param is valid did/handle format. 12 + // If not, respond with 404 13 + func EnsureDidOrHandle(p *pages.Pages) middlewareFunc { 14 + return func(next http.Handler) http.Handler { 15 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 + user := chi.URLParam(r, "user") 17 + 18 + // if using a DID or handle, just continue as per usual 19 + if userutil.IsDid(user) || userutil.IsHandle(user) { 20 + next.ServeHTTP(w, r) 21 + return 22 + } 23 + 24 + // TODO: run Normalize middleware from here 25 + 26 + p.Error404(w) 27 + return 28 + }) 29 + } 30 + }
+50
appview/web/middleware/normalize.go
··· 1 + package middleware 2 + 3 + import ( 4 + "net/http" 5 + "strings" 6 + 7 + "github.com/go-chi/chi/v5" 8 + "tangled.org/core/appview/state/userutil" 9 + ) 10 + 11 + func Normalize() middlewareFunc { 12 + return func(next http.Handler) http.Handler { 13 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 + pat := chi.URLParam(r, "*") 15 + pathParts := strings.SplitN(pat, "/", 2) 16 + if len(pathParts) == 0 { 17 + next.ServeHTTP(w, r) 18 + return 19 + } 20 + 21 + firstPart := pathParts[0] 22 + 23 + // if using a flattened DID (like you would in go modules), unflatten 24 + if userutil.IsFlattenedDid(firstPart) { 25 + unflattenedDid := userutil.UnflattenDid(firstPart) 26 + redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/") 27 + 28 + redirectURL := *r.URL 29 + redirectURL.Path = "/" + redirectPath 30 + 31 + http.Redirect(w, r, redirectURL.String(), http.StatusFound) 32 + return 33 + } 34 + 35 + // if using a handle with @, rewrite to work without @ 36 + if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) { 37 + redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/") 38 + 39 + redirectURL := *r.URL 40 + redirectURL.Path = "/" + redirectPath 41 + 42 + http.Redirect(w, r, redirectURL.String(), http.StatusFound) 43 + return 44 + } 45 + 46 + next.ServeHTTP(w, r) 47 + return 48 + }) 49 + } 50 + }
+39
appview/web/request/context.go
··· 1 + package request 2 + 3 + import ( 4 + "context" 5 + 6 + "github.com/bluesky-social/indigo/atproto/identity" 7 + "tangled.org/core/appview/models" 8 + ) 9 + 10 + type ctxKeyOwner struct{} 11 + type ctxKeyRepo struct{} 12 + type ctxKeyIssue struct{} 13 + 14 + func WithOwner(ctx context.Context, owner *identity.Identity) context.Context { 15 + return context.WithValue(ctx, ctxKeyOwner{}, owner) 16 + } 17 + 18 + func OwnerFromContext(ctx context.Context) (*identity.Identity, bool) { 19 + owner, ok := ctx.Value(ctxKeyOwner{}).(*identity.Identity) 20 + return owner, ok 21 + } 22 + 23 + func WithRepo(ctx context.Context, repo *models.Repo) context.Context { 24 + return context.WithValue(ctx, ctxKeyRepo{}, repo) 25 + } 26 + 27 + func RepoFromContext(ctx context.Context) (*models.Repo, bool) { 28 + repo, ok := ctx.Value(ctxKeyRepo{}).(*models.Repo) 29 + return repo, ok 30 + } 31 + 32 + func WithIssue(ctx context.Context, issue *models.Issue) context.Context { 33 + return context.WithValue(ctx, ctxKeyIssue{}, issue) 34 + } 35 + 36 + func IssueFromContext(ctx context.Context) (*models.Issue, bool) { 37 + issue, ok := ctx.Value(ctxKeyIssue{}).(*models.Issue) 38 + return issue, ok 39 + }
+2 -1
cmd/appview/main.go
··· 7 7 8 8 "tangled.org/core/appview/config" 9 9 "tangled.org/core/appview/state" 10 + "tangled.org/core/appview/web" 10 11 tlog "tangled.org/core/log" 11 12 ) 12 13 ··· 35 36 36 37 logger.Info("starting server", "address", c.Core.ListenAddr) 37 38 38 - if err := http.ListenAndServe(c.Core.ListenAddr, state.Router()); err != nil { 39 + if err := http.ListenAndServe(c.Core.ListenAddr, web.RouterFromState(state)); err != nil { 39 40 logger.Error("failed to start appview", "err", err) 40 41 } 41 42 }

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