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
+840
Diff #0
+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 + }
+213
appview/service/issue/issue.go
··· 1 + package issue 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "log/slog" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 12 + "tangled.org/core/api/tangled" 13 + "tangled.org/core/appview/config" 14 + "tangled.org/core/appview/db" 15 + "tangled.org/core/appview/models" 16 + "tangled.org/core/appview/notify" 17 + "tangled.org/core/appview/refresolver" 18 + "tangled.org/core/tid" 19 + ) 20 + 21 + type IssueService struct { 22 + logger *slog.Logger 23 + config *config.Config 24 + db *db.DB 25 + notifier notify.Notifier 26 + refResolver *refresolver.Resolver 27 + } 28 + 29 + func NewService( 30 + logger *slog.Logger, 31 + config *config.Config, 32 + db *db.DB, 33 + notifier notify.Notifier, 34 + refResolver *refresolver.Resolver, 35 + ) IssueService { 36 + return IssueService{ 37 + logger, 38 + config, 39 + db, 40 + notifier, 41 + refResolver, 42 + } 43 + } 44 + 45 + var ( 46 + ErrCtxMissing = errors.New("context values are missing") 47 + ErrDatabaseFail = errors.New("db op fail") 48 + ErrPDSFail = errors.New("pds op fail") 49 + ErrValidationFail = errors.New("issue validation fail") 50 + ) 51 + 52 + // TODO: NewIssue should return typed errors 53 + func (s *IssueService) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) { 54 + l := s.logger.With("method", "NewIssue") 55 + sess, ok := fromContext(ctx) 56 + if !ok { 57 + l.Error("user session is missing in context") 58 + return nil, ErrCtxMissing 59 + } 60 + authorDid := sess.Data.AccountDID 61 + l = l.With("did", authorDid) 62 + 63 + mentions, references := s.refResolver.Resolve(ctx, body) 64 + 65 + 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, 76 + } 77 + // TODO: validate the issue 78 + 79 + tx, err := s.db.BeginTx(ctx, nil) 80 + if err != nil { 81 + l.Error("db.BeginTx failed", "err", err) 82 + return nil, ErrDatabaseFail 83 + } 84 + defer tx.Rollback() 85 + 86 + if err := db.PutIssue(tx, &issue); err != nil { 87 + l.Error("db.PutIssue failed", "err", err) 88 + return nil, ErrDatabaseFail 89 + } 90 + 91 + atpclient := sess.APIClient() 92 + record := issue.AsRecord() 93 + _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 94 + Repo: authorDid.String(), 95 + Collection: tangled.RepoIssueNSID, 96 + Rkey: issue.Rkey, 97 + Record: &lexutil.LexiconTypeDecoder{ 98 + Val: &record, 99 + }, 100 + }) 101 + if err != nil { 102 + l.Error("atproto.RepoPutRecord failed", "err", err) 103 + return nil, ErrPDSFail 104 + } 105 + if err = tx.Commit(); err != nil { 106 + l.Error("tx.Commit failed", "err", err) 107 + return nil, ErrDatabaseFail 108 + } 109 + 110 + s.notifier.NewIssue(ctx, &issue, mentions) 111 + return &issue, nil 112 + } 113 + 114 + func (s *IssueService) EditIssue(ctx context.Context, issue *models.Issue) error { 115 + l := s.logger.With("method", "EditIssue") 116 + sess, ok := fromContext(ctx) 117 + if !ok { 118 + l.Error("user session is missing in context") 119 + return ErrCtxMissing 120 + } 121 + authorDid := sess.Data.AccountDID 122 + l = l.With("did", authorDid) 123 + 124 + // TODO: validate issue 125 + 126 + tx, err := s.db.BeginTx(ctx, nil) 127 + if err != nil { 128 + l.Error("db.BeginTx failed", "err", err) 129 + return ErrDatabaseFail 130 + } 131 + defer tx.Rollback() 132 + 133 + if err := db.PutIssue(tx, issue); err != nil { 134 + l.Error("db.PutIssue failed", "err", err) 135 + return ErrDatabaseFail 136 + } 137 + 138 + atpclient := sess.APIClient() 139 + record := issue.AsRecord() 140 + 141 + ex, err := atproto.RepoGetRecord(ctx, atpclient, "", tangled.RepoIssueNSID, issue.Did, issue.Rkey) 142 + if err != nil { 143 + l.Error("atproto.RepoGetRecord failed", "err", err) 144 + return ErrPDSFail 145 + } 146 + _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 147 + Collection: tangled.RepoIssueNSID, 148 + SwapRecord: ex.Cid, 149 + Record: &lexutil.LexiconTypeDecoder{ 150 + Val: &record, 151 + }, 152 + }) 153 + if err != nil { 154 + l.Error("atproto.RepoPutRecord failed", "err", err) 155 + return ErrPDSFail 156 + } 157 + 158 + if err = tx.Commit(); err != nil { 159 + l.Error("tx.Commit failed", "err", err) 160 + return ErrDatabaseFail 161 + } 162 + 163 + // TODO: notify PutIssue 164 + 165 + return nil 166 + } 167 + 168 + func (s *IssueService) DeleteIssue(ctx context.Context, issue *models.Issue) error { 169 + l := s.logger.With("method", "DeleteIssue") 170 + sess, ok := fromContext(ctx) 171 + if !ok { 172 + return ErrCtxMissing 173 + } 174 + authorDid := sess.Data.AccountDID 175 + l = l.With("did", authorDid) 176 + 177 + tx, err := s.db.BeginTx(ctx, nil) 178 + if err != nil { 179 + l.Error("db.BeginTx failed", "err", err) 180 + return ErrDatabaseFail 181 + } 182 + defer tx.Rollback() 183 + 184 + if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil { 185 + l.Error("db.DeleteIssues failed", "err", err) 186 + return ErrDatabaseFail 187 + } 188 + 189 + atpclient := sess.APIClient() 190 + _, err = atproto.RepoDeleteRecord(ctx, atpclient, &atproto.RepoDeleteRecord_Input{ 191 + Collection: tangled.RepoIssueNSID, 192 + Repo: issue.Did, 193 + Rkey: issue.Rkey, 194 + }) 195 + if err != nil { 196 + l.Error("atproto.RepoDeleteRecord failed", "err", err) 197 + return ErrPDSFail 198 + } 199 + 200 + if err := tx.Commit(); err != nil { 201 + l.Error("tx.Commit failed", "err", err) 202 + return ErrDatabaseFail 203 + } 204 + 205 + s.notifier.DeleteIssue(ctx, issue) 206 + return nil 207 + } 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 + }
+15
appview/service/issue/state.go
··· 1 + package issue 2 + 3 + import ( 4 + "context" 5 + 6 + "tangled.org/core/appview/models" 7 + ) 8 + 9 + func (s *IssueService) CloseIssue(ctx context.Context, iusse *models.Issue) error { 10 + panic("unimplemented") 11 + } 12 + 13 + func (s *IssueService) ReopenIssue(ctx context.Context, iusse *models.Issue) error { 14 + panic("unimplemented") 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 + }
+71
appview/service/repo/repo.go
··· 1 + package repo 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log/slog" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 + "tangled.org/core/api/tangled" 12 + "tangled.org/core/appview/config" 13 + "tangled.org/core/appview/db" 14 + "tangled.org/core/appview/models" 15 + "tangled.org/core/rbac" 16 + "tangled.org/core/tid" 17 + ) 18 + 19 + type RepoService struct { 20 + logger *slog.Logger 21 + config *config.Config 22 + db *db.DB 23 + enforcer *rbac.Enforcer 24 + } 25 + 26 + // NewRepo creates a repository 27 + // It expects atproto session to be passed in `ctx` 28 + func (s *RepoService) NewRepo(ctx context.Context, name, description, knot string) error { 29 + l := s.logger.With("method", "NewRepo") 30 + sess := fromContext(ctx) 31 + 32 + ownerDid := sess.Data.AccountDID 33 + l = l.With("did", ownerDid) 34 + 35 + repo := models.Repo{ 36 + Did: ownerDid.String(), 37 + Name: name, 38 + Knot: knot, 39 + Rkey: tid.TID(), 40 + Description: description, 41 + Created: time.Now(), 42 + Labels: s.config.Label.DefaultLabelDefs, 43 + } 44 + l = l.With("aturi", repo.RepoAt()) 45 + 46 + tx, err := s.db.BeginTx(ctx, nil) 47 + if err != nil { 48 + return fmt.Errorf("db.BeginTx: %w", err) 49 + } 50 + defer tx.Rollback() 51 + 52 + 53 + atpclient := sess.APIClient() 54 + _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 55 + Collection: tangled.RepoNSID, 56 + Repo: repo.Did, 57 + }) 58 + if err != nil { 59 + return fmt.Errorf("atproto.RepoPutRecord: %w", err) 60 + } 61 + l.Info("wrote to PDS") 62 + 63 + // knotclient, err := s.oauth.ServiceClient( 64 + // ) 65 + 66 + return nil 67 + } 68 + 69 + func fromContext(ctx context.Context) oauth.ClientSession { 70 + panic("todo") 71 + }
+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" 21 22 "tangled.org/core/log" 22 23 ) 23 24 ··· 40 41 userRouter := s.UserRouter(&middleware) 41 42 standardRouter := s.StandardRouter(&middleware) 42 43 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 + 43 55 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 44 56 pat := chi.URLParam(r, "*") 45 57 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 +
+51
appview/web/middleware/auth.go
··· 1 + package middleware 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log" 7 + "net/http" 8 + "net/url" 9 + 10 + "tangled.org/core/appview/oauth" 11 + ) 12 + 13 + func AuthMiddleware(o *oauth.OAuth) middlewareFunc { 14 + return func(next http.Handler) http.Handler { 15 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 + returnURL := "/" 17 + if u, err := url.Parse(r.Header.Get("Referer")); err == nil { 18 + returnURL = u.RequestURI() 19 + } 20 + 21 + loginURL := fmt.Sprintf("/login?return_url=%s", url.QueryEscape(returnURL)) 22 + 23 + redirectFunc := func(w http.ResponseWriter, r *http.Request) { 24 + http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) 25 + } 26 + if r.Header.Get("HX-Request") == "true" { 27 + redirectFunc = func(w http.ResponseWriter, _ *http.Request) { 28 + w.Header().Set("HX-Redirect", loginURL) 29 + w.WriteHeader(http.StatusOK) 30 + } 31 + } 32 + 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 + 40 + if sess == nil { 41 + log.Printf("session is nil, redirecting...") 42 + redirectFunc(w, r) 43 + return 44 + } 45 + 46 + // TODO: use IntoContext instead 47 + ctx := context.WithValue(r.Context(), "sess", sess) 48 + next.ServeHTTP(w, r.WithContext(ctx)) 49 + }) 50 + } 51 + }
+18
appview/web/middleware/log.go
··· 1 + package middleware 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + 7 + "tangled.org/core/log" 8 + ) 9 + 10 + func WithLogger(l *slog.Logger) middlewareFunc { 11 + return func(next http.Handler) http.Handler { 12 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 + // NOTE: can add some metadata here 14 + ctx := log.IntoContext(r.Context(), l) 15 + next.ServeHTTP(w, r.WithContext(ctx)) 16 + }) 17 + } 18 + }
+7
appview/web/middleware/middleware.go
··· 1 + package middleware 2 + 3 + import ( 4 + "net/http" 5 + ) 6 + 7 + type middlewareFunc func(http.Handler) http.Handler
+38
appview/web/middleware/paginate.go
··· 1 + package middleware 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "strconv" 7 + 8 + "tangled.org/core/appview/pagination" 9 + ) 10 + 11 + func Paginate(next http.Handler) http.Handler { 12 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 + page := pagination.FirstPage() 14 + 15 + offsetVal := r.URL.Query().Get("offset") 16 + if offsetVal != "" { 17 + offset, err := strconv.Atoi(offsetVal) 18 + if err != nil { 19 + log.Println("invalid offset") 20 + } else { 21 + page.Offset = offset 22 + } 23 + } 24 + 25 + limitVal := r.URL.Query().Get("limit") 26 + if limitVal != "" { 27 + limit, err := strconv.Atoi(limitVal) 28 + if err != nil { 29 + log.Println("invalid limit") 30 + } else { 31 + page.Limit = limit 32 + } 33 + } 34 + 35 + ctx := pagination.IntoContext(r.Context(), page) 36 + next.ServeHTTP(w, r.WithContext(ctx)) 37 + }) 38 + }
+114
appview/web/middleware/resolve.go
··· 1 + package middleware 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "strconv" 7 + "strings" 8 + 9 + "github.com/go-chi/chi/v5" 10 + "tangled.org/core/appview/db" 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" 15 + "tangled.org/core/idresolver" 16 + ) 17 + 18 + func ResolveIdent( 19 + idResolver *idresolver.Resolver, 20 + pages *pages.Pages, 21 + ) middlewareFunc { 22 + return func(next http.Handler) http.Handler { 23 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 + didOrHandle := chi.URLParam(r, "user") 25 + didOrHandle = strings.TrimPrefix(didOrHandle, "@") 26 + 27 + id, err := idResolver.ResolveIdent(r.Context(), didOrHandle) 28 + if err != nil { 29 + // invalid did or handle 30 + log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err) 31 + pages.Error404(w) 32 + return 33 + } 34 + 35 + ctx := owner_service.IntoContext(r.Context(), id) 36 + log.Println("ident resolved") 37 + 38 + next.ServeHTTP(w, r.WithContext(ctx)) 39 + }) 40 + } 41 + } 42 + 43 + func ResolveRepo( 44 + e *db.DB, 45 + idResolver *idresolver.Resolver, 46 + pages *pages.Pages, 47 + ) middlewareFunc { 48 + return func(next http.Handler) http.Handler { 49 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 + repoName := chi.URLParam(r, "repo") 51 + repoOwner, ok := owner_service.FromContext(r.Context()) 52 + if !ok { 53 + log.Println("malformed middleware") 54 + w.WriteHeader(http.StatusInternalServerError) 55 + return 56 + } 57 + 58 + repo, err := db.GetRepo( 59 + e, 60 + db.FilterEq("did", repoOwner.DID.String()), 61 + db.FilterEq("name", repoName), 62 + ) 63 + if err != nil { 64 + log.Println("failed to resolve repo", "err", err) 65 + pages.ErrorKnot404(w) 66 + return 67 + } 68 + 69 + // TODO: pass owner id into repository object 70 + 71 + ctx := repo_service.IntoContext(r.Context(), repo) 72 + log.Println("repo resolved") 73 + 74 + next.ServeHTTP(w, r.WithContext(ctx)) 75 + }) 76 + } 77 + } 78 + 79 + func ResolveIssue( 80 + e *db.DB, 81 + idResolver *idresolver.Resolver, 82 + pages *pages.Pages, 83 + ) middlewareFunc { 84 + return func(next http.Handler) http.Handler { 85 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 + issueIdStr := chi.URLParam(r, "issue") 87 + issueId, err := strconv.Atoi(issueIdStr) 88 + if err != nil { 89 + log.Println("failed to fully resolve issue ID", err) 90 + pages.Error404(w) 91 + return 92 + } 93 + repo, ok := repo_service.FromContext(r.Context()) 94 + if !ok { 95 + log.Println("malformed middleware") 96 + w.WriteHeader(http.StatusInternalServerError) 97 + return 98 + } 99 + 100 + issue, err := db.GetIssue(e, repo.RepoAt(), issueId) 101 + if err != nil { 102 + log.Println("failed to resolve repo", "err", err) 103 + pages.ErrorKnot404(w) 104 + return 105 + } 106 + issue.Repo = repo 107 + 108 + ctx := issue_service.IntoContext(r.Context(), issue) 109 + log.Println("issue resolved") 110 + 111 + next.ServeHTTP(w, r.WithContext(ctx)) 112 + }) 113 + } 114 + }
+89
appview/web/routes.go
··· 1 + package web 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + 7 + "github.com/go-chi/chi/v5" 8 + "tangled.org/core/appview/config" 9 + "tangled.org/core/appview/db" 10 + "tangled.org/core/appview/notify" 11 + "tangled.org/core/appview/oauth" 12 + "tangled.org/core/appview/pages" 13 + "tangled.org/core/appview/refresolver" 14 + "tangled.org/core/appview/service/issue" 15 + "tangled.org/core/appview/web/handler" 16 + "tangled.org/core/appview/web/middleware" 17 + "tangled.org/core/idresolver" 18 + ) 19 + 20 + // obviously this is directly against to the go convention, though I think 21 + // flattening all handler files can significantly reduce the effort involved in 22 + // file naming and grouping. We are already grouping services by domains, and 23 + // doing same to web handlers is just over complicating imo 24 + 25 + // Rules 26 + // - use single function for each endpoints (unless it doesn't make sense) 27 + // - pass dependencies to each handlers, don't share dependencies 28 + 29 + func UserRouter( 30 + // NOTE: put base dependencies (db, idResolver, oauth etc) 31 + logger *slog.Logger, 32 + config *config.Config, 33 + db *db.DB, 34 + idResolver *idresolver.Resolver, 35 + refResolver *refresolver.Resolver, 36 + notifier notify.Notifier, 37 + oauth *oauth.OAuth, 38 + pages *pages.Pages, 39 + ) http.Handler { 40 + r := chi.NewRouter() 41 + 42 + auth := middleware.AuthMiddleware(oauth) 43 + 44 + issue := issue.NewService( 45 + logger, 46 + config, 47 + db, 48 + notifier, 49 + refResolver, 50 + ) 51 + 52 + r.Use(middleware.WithLogger(logger)) 53 + 54 + r.Route("/{user}", func(r chi.Router) { 55 + r.Use(middleware.ResolveIdent(idResolver, pages)) 56 + 57 + // r.Get("/", Profile) 58 + // r.Get("/feed.atom", AtomFeedPage) 59 + 60 + r.Route("/{repo}", func(r chi.Router) { 61 + r.Use(middleware.ResolveRepo(db, idResolver, pages)) 62 + 63 + // /{user}/{repo}/issues/* 64 + r.With(middleware.Paginate).Get("/issues", handler.RepoIssues(issue)) 65 + r.With(auth).Get("/issues/new", handler.NewIssue(pages)) 66 + r.With(auth).Post("/issues/new", handler.NewIssuePost(issue, pages)) 67 + r.Route("/issues/{issue}", func(r chi.Router) { 68 + r.Use(middleware.ResolveIssue(db, idResolver, pages)) 69 + 70 + r.Get("/", handler.Issue(issue)) 71 + r.Get("/opengraph", handler.IssueOpenGraph(issue)) 72 + 73 + r.With(auth).Delete("/", handler.IssueDelete(issue, pages)) 74 + 75 + r.With(auth).Get("/edit", handler.IssueEdit(issue)) 76 + r.With(auth).Post("/edit", handler.IssueEditPost(issue)) 77 + 78 + r.With(auth).Post("/close", handler.CloseIssue(issue)) 79 + r.With(auth).Post("/reopen", handler.ReopenIssue(issue)) 80 + 81 + // TODO: comments 82 + }) 83 + 84 + // TODO: put more routes 85 + }) 86 + }) 87 + 88 + return r 89 + }

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
boltless.me submitted #0
1 commit
expand
draft: appview: service layer
3/3 failed
expand
expand 0 comments