back interdiff of round #2 and #1

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

REVERTED
appview/service/issue/context.go
···
-
package issue
-
-
import (
-
"context"
-
-
"tangled.org/core/appview/models"
-
)
-
-
type ctxKey struct{}
-
-
func IntoContext(ctx context.Context, repo *models.Issue) context.Context {
-
return context.WithValue(ctx, ctxKey{}, repo)
-
}
-
-
func FromContext(ctx context.Context) (*models.Issue, bool) {
-
repo, ok := ctx.Value(ctxKey{}).(*models.Issue)
-
return repo, ok
-
}
ERROR
appview/service/issue/issue.go

Failed to calculate interdiff for this file.

ERROR
appview/service/issue/state.go

Failed to calculate interdiff for this file.

REVERTED
appview/service/owner/context.go
···
-
package owner
-
-
import (
-
"context"
-
-
"github.com/bluesky-social/indigo/atproto/identity"
-
)
-
-
type ctxKey struct{}
-
-
func IntoContext(ctx context.Context, id *identity.Identity) context.Context {
-
return context.WithValue(ctx, ctxKey{}, id)
-
}
-
-
func FromContext(ctx context.Context) (*identity.Identity, bool) {
-
repo, ok := ctx.Value(ctxKey{}).(*identity.Identity)
-
return repo, ok
-
}
REVERTED
appview/service/repo/context.go
···
-
package repo
-
-
import (
-
"context"
-
-
"tangled.org/core/appview/models"
-
)
-
-
type ctxKey struct{}
-
-
func IntoContext(ctx context.Context, repo *models.Repo) context.Context {
-
return context.WithValue(ctx, ctxKey{}, repo)
-
}
-
-
func FromContext(ctx context.Context) (*models.Repo, bool) {
-
repo, ok := ctx.Value(ctxKey{}).(*models.Repo)
-
return repo, ok
-
}
ERROR
appview/service/repo/repo.go

Failed to calculate interdiff for this file.

REVERTED
appview/state/router.go
···
"tangled.org/core/appview/spindles"
"tangled.org/core/appview/state/userutil"
avstrings "tangled.org/core/appview/strings"
-
"tangled.org/core/appview/web"
"tangled.org/core/log"
)
···
userRouter := s.UserRouter(&middleware)
standardRouter := s.StandardRouter(&middleware)
-
_ = web.UserRouter(
-
s.logger,
-
s.config,
-
s.db,
-
s.idResolver,
-
s.refResolver,
-
s.notifier,
-
s.oauth,
-
s.pages,
-
)
-
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
pat := chi.URLParam(r, "*")
pathParts := strings.SplitN(pat, "/", 2)
REVERTED
appview/web/handler/issues.go
···
-
package handler
-
-
import (
-
"net/http"
-
-
"tangled.org/core/appview/service/issue"
-
)
-
-
func RepoIssues(s issue.IssueService) http.HandlerFunc {
-
return func(w http.ResponseWriter, r *http.Request) {
-
}
-
}
REVERTED
appview/web/handler/issues_issue.go
···
-
package handler
-
-
import (
-
"net/http"
-
-
"tangled.org/core/appview/pages"
-
isvc "tangled.org/core/appview/service/issue"
-
"tangled.org/core/log"
-
)
-
-
func Issue(s isvc.IssueService) http.HandlerFunc {
-
return func(w http.ResponseWriter, r *http.Request) {
-
panic("unimplemented")
-
}
-
}
-
-
func IssueDelete(s isvc.IssueService, p *pages.Pages) http.HandlerFunc {
-
noticeId := "issue-actions-error"
-
return func(w http.ResponseWriter, r *http.Request) {
-
ctx := r.Context()
-
l := log.FromContext(ctx).With("handler", "IssueDelete")
-
issue, ok := isvc.FromContext(ctx)
-
if !ok {
-
l.Error("failed to get issue")
-
// TODO: 503 error with more detailed messages
-
p.Error503(w)
-
return
-
}
-
err := s.DeleteIssue(ctx, issue)
-
if err != nil {
-
p.Notice(w, noticeId, "failed to delete issue")
-
}
-
p.HxLocation(w, "/")
-
}
-
}
REVERTED
appview/web/handler/issues_issue_close.go
···
-
package handler
-
-
import (
-
"net/http"
-
-
"tangled.org/core/appview/service/issue"
-
)
-
-
func CloseIssue(s issue.IssueService) http.HandlerFunc {
-
return func(w http.ResponseWriter, r *http.Request) {
-
panic("unimplemented")
-
}
-
}
REVERTED
appview/web/handler/issues_issue_edit.go
···
-
package handler
-
-
import (
-
"net/http"
-
-
"tangled.org/core/appview/service/issue"
-
)
-
-
func IssueEdit(s issue.IssueService) http.HandlerFunc {
-
return func(w http.ResponseWriter, r *http.Request) {
-
panic("unimplemented")
-
}
-
}
-
-
func IssueEditPost(s issue.IssueService) http.HandlerFunc {
-
return func(w http.ResponseWriter, r *http.Request) {
-
panic("unimplemented")
-
}
-
}
REVERTED
appview/web/handler/issues_issue_opengraph.go
···
-
package handler
-
-
import (
-
"net/http"
-
-
"tangled.org/core/appview/service/issue"
-
)
-
-
func IssueOpenGraph(s issue.IssueService) http.HandlerFunc {
-
return func(w http.ResponseWriter, r *http.Request) {
-
panic("unimplemented")
-
}
-
}
REVERTED
appview/web/handler/issues_issue_reopen.go
···
-
package handler
-
-
import (
-
"net/http"
-
-
"tangled.org/core/appview/service/issue"
-
)
-
-
func ReopenIssue(s issue.IssueService) http.HandlerFunc {
-
return func(w http.ResponseWriter, r *http.Request) {
-
panic("unimplemented")
-
}
-
}
REVERTED
appview/web/handler/issues_new.go
···
-
package handler
-
-
import (
-
"errors"
-
"net/http"
-
-
"tangled.org/core/appview/pages"
-
isvc "tangled.org/core/appview/service/issue"
-
"tangled.org/core/appview/service/repo"
-
"tangled.org/core/log"
-
)
-
-
func NewIssue(p *pages.Pages) http.HandlerFunc {
-
return func(w http.ResponseWriter, r *http.Request) {
-
// TODO: render page
-
}
-
}
-
-
func NewIssuePost(is isvc.IssueService, p *pages.Pages) http.HandlerFunc {
-
noticeId := "issues"
-
return func(w http.ResponseWriter, r *http.Request) {
-
ctx := r.Context()
-
l := log.FromContext(ctx).With("handler", "NewIssuePost")
-
repo, ok := repo.FromContext(ctx)
-
if !ok {
-
l.Error("failed to get repo")
-
// TODO: 503 error with more detailed messages
-
p.Error503(w)
-
return
-
}
-
var (
-
title = r.FormValue("title")
-
body = r.FormValue("body")
-
)
-
-
_, err := is.NewIssue(ctx, repo, title, body)
-
if err != nil {
-
if errors.Is(err, isvc.ErrDatabaseFail) {
-
p.Notice(w, noticeId, "Failed to create issue.")
-
} else if errors.Is(err, isvc.ErrPDSFail) {
-
p.Notice(w, noticeId, "Failed to create issue.")
-
} else {
-
p.Notice(w, noticeId, "Failed to create issue.")
-
}
-
}
-
p.HxLocation(w, "/")
-
}
-
}
REVERTED
appview/web/handler/repos_new.go
···
-
package handler
REVERTED
appview/web/handler/repos_repo.go
···
-
package handler
-
REVERTED
appview/web/handler/repos_repo_opengraph.go
···
-
package handler
-
ERROR
appview/web/middleware/auth.go

Failed to calculate interdiff for this file.

ERROR
appview/web/middleware/log.go

Failed to calculate interdiff for this file.

ERROR
appview/web/middleware/middleware.go

Failed to calculate interdiff for this file.

ERROR
appview/web/middleware/paginate.go

Failed to calculate interdiff for this file.

ERROR
appview/web/middleware/resolve.go

Failed to calculate interdiff for this file.

ERROR
appview/web/routes.go

Failed to calculate interdiff for this file.

NEW
appview/oauth/handler.go
···
r.Get("/oauth/client-metadata.json", o.clientMetadata)
r.Get("/oauth/jwks.json", o.jwks)
-
r.Get("/oauth/callback", o.callback)
+
r.Get("/oauth/callback", o.Callback)
return r
}
···
}
}
-
func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) {
+
func (o *OAuth) Callback(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
l := o.Logger.With("query", r.URL.Query())
NEW
appview/oauth/session.go
···
+
package oauth
+
+
import (
+
"net/http"
+
+
"github.com/bluesky-social/indigo/atproto/auth/oauth"
+
)
+
+
func (o *OAuth) SaveSession2(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) {
+
}
NEW
appview/service/repo/repoinfo.go
···
+
package repo
+
+
import (
+
"context"
+
+
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
+
"tangled.org/core/appview/oauth"
+
"tangled.org/core/appview/pages/repoinfo"
+
)
+
+
// GetRepoInfo converts given `Repo` to `RepoInfo` object.
+
// The `user` can be nil.
+
func (s *Service) GetRepoInfo(ctx context.Context, baseRepo *models.Repo, user *oauth.User) (*repoinfo.RepoInfo, error) {
+
var (
+
repoAt = baseRepo.RepoAt()
+
isStarred = false
+
roles = repoinfo.RolesInRepo{}
+
)
+
if user != nil {
+
isStarred = db.GetStarStatus(s.db, user.Did, repoAt)
+
roles.Roles = s.enforcer.GetPermissionsInRepo(user.Did, baseRepo.Knot, baseRepo.DidSlashRepo())
+
}
+
+
stats := baseRepo.RepoStats
+
if stats == nil {
+
starCount, err := db.GetStarCount(s.db, repoAt)
+
if err != nil {
+
return nil, err
+
}
+
issueCount, err := db.GetIssueCount(s.db, repoAt)
+
if err != nil {
+
return nil, err
+
}
+
pullCount, err := db.GetPullCount(s.db, repoAt)
+
if err != nil {
+
return nil, err
+
}
+
stats = &models.RepoStats{
+
StarCount: starCount,
+
IssueCount: issueCount,
+
PullCount: pullCount,
+
}
+
}
+
+
var sourceRepo *models.Repo
+
var err error
+
if baseRepo.Source != "" {
+
sourceRepo, err = db.GetRepoByAtUri(s.db, baseRepo.Source)
+
if err != nil {
+
return nil, err
+
}
+
}
+
+
repoInfo := &repoinfo.RepoInfo{
+
// ok this is basically a models.Repo
+
OwnerDid: baseRepo.Did,
+
OwnerHandle: "", // TODO: shouldn't use
+
Name: baseRepo.Name,
+
Rkey: baseRepo.Rkey,
+
Description: baseRepo.Description,
+
Website: baseRepo.Website,
+
Topics: baseRepo.Topics,
+
Knot: baseRepo.Knot,
+
Spindle: baseRepo.Spindle,
+
Stats: *stats,
+
+
// fork repo upstream
+
Source: sourceRepo,
+
+
// repo path (context)
+
CurrentDir: "",
+
Ref: "",
+
+
// info related to the session
+
IsStarred: isStarred,
+
Roles: roles,
+
}
+
+
return repoInfo, nil
+
}
NEW
appview/session/context.go
···
+
package session
+
+
import (
+
"context"
+
+
toauth "tangled.org/core/appview/oauth"
+
)
+
+
type ctxKey struct{}
+
+
func IntoContext(ctx context.Context, sess Session) context.Context {
+
return context.WithValue(ctx, ctxKey{}, &sess)
+
}
+
+
func FromContext(ctx context.Context) *Session {
+
sess, ok := ctx.Value(ctxKey{}).(*Session)
+
if !ok {
+
return nil
+
}
+
return sess
+
}
+
+
func UserFromContext(ctx context.Context) *toauth.User {
+
sess := FromContext(ctx)
+
if sess == nil {
+
return nil
+
}
+
return sess.User()
+
}
NEW
appview/session/session.go
···
+
package session
+
+
import (
+
"github.com/bluesky-social/indigo/atproto/auth/oauth"
+
toauth "tangled.org/core/appview/oauth"
+
)
+
+
// Session is a lightweight wrapper over indigo-oauth ClientSession
+
type Session struct {
+
*oauth.ClientSession
+
}
+
+
func New(atSess *oauth.ClientSession) Session {
+
return Session{
+
atSess,
+
}
+
}
+
+
func (s *Session) User() *toauth.User {
+
return &toauth.User{
+
Did: string(s.Data.AccountDID),
+
Pds: s.Data.HostURL,
+
}
+
}
NEW
appview/state/legacy_bridge.go
···
+
package state
+
+
import (
+
"log/slog"
+
+
"tangled.org/core/appview/config"
+
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/indexer"
+
"tangled.org/core/appview/middleware"
+
"tangled.org/core/appview/notify"
+
"tangled.org/core/appview/oauth"
+
"tangled.org/core/appview/pages"
+
"tangled.org/core/appview/reporesolver"
+
"tangled.org/core/appview/validator"
+
"tangled.org/core/idresolver"
+
"tangled.org/core/rbac"
+
)
+
+
// Expose exposes private fields in `State`. This is used to bridge between
+
// legacy web routers and new architecture
+
func (s *State) Expose() (
+
*config.Config,
+
*db.DB,
+
*rbac.Enforcer,
+
*idresolver.Resolver,
+
*indexer.Indexer,
+
*slog.Logger,
+
notify.Notifier,
+
*oauth.OAuth,
+
*pages.Pages,
+
*reporesolver.RepoResolver,
+
*validator.Validator,
+
) {
+
return s.config, s.db, s.enforcer, s.idResolver, s.indexer, s.logger, s.notifier, s.oauth, s.pages, s.repoResolver, s.validator
+
}
+
+
func (s *State) Middleware() *middleware.Middleware {
+
mw := middleware.New(
+
s.oauth,
+
s.db,
+
s.enforcer,
+
s.repoResolver,
+
s.idResolver,
+
s.pages,
+
)
+
return &mw
+
}
NEW
appview/web/handler/oauth_client_metadata.go
···
+
package handler
+
+
import (
+
"encoding/json"
+
"net/http"
+
+
"tangled.org/core/appview/oauth"
+
)
+
+
func OauthClientMetadata(o *oauth.OAuth) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
doc := o.ClientApp.Config.ClientMetadata()
+
doc.JWKSURI = &o.JwksUri
+
doc.ClientName = &o.ClientName
+
doc.ClientURI = &o.ClientUri
+
+
w.Header().Set("Content-Type", "application/json")
+
if err := json.NewEncoder(w).Encode(doc); err != nil {
+
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
}
+
}
NEW
appview/web/handler/oauth_jwks.go
···
+
package handler
+
+
import (
+
"encoding/json"
+
"net/http"
+
+
"tangled.org/core/appview/oauth"
+
)
+
+
func OauthJwks(o *oauth.OAuth) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
w.Header().Set("Content-Type", "application/json")
+
body := o.ClientApp.Config.PublicJWKS()
+
if err := json.NewEncoder(w).Encode(body); err != nil {
+
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
}
+
}
NEW
appview/web/handler/user_repo_issues.go
···
+
package handler
+
+
import (
+
"net/http"
+
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
+
"tangled.org/core/appview/pages"
+
"tangled.org/core/appview/pagination"
+
isvc "tangled.org/core/appview/service/issue"
+
rsvc "tangled.org/core/appview/service/repo"
+
"tangled.org/core/appview/session"
+
"tangled.org/core/appview/web/request"
+
"tangled.org/core/log"
+
)
+
+
func RepoIssues(is isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
ctx := r.Context()
+
l := log.FromContext(ctx).With("handler", "RepoIssues")
+
repo, ok := request.RepoFromContext(ctx)
+
if !ok {
+
l.Error("malformed request")
+
p.Error503(w)
+
return
+
}
+
+
query := r.URL.Query()
+
searchOpts := models.IssueSearchOptions{
+
RepoAt: repo.RepoAt().String(),
+
Keyword: query.Get("q"),
+
IsOpen: query.Get("state") != "closed",
+
Page: pagination.FromContext(ctx),
+
}
+
+
issues, err := is.GetIssues(ctx, repo, searchOpts)
+
if err != nil {
+
l.Error("failed to get issues")
+
p.Error503(w)
+
return
+
}
+
+
// render page
+
err = func() error {
+
user := session.UserFromContext(ctx)
+
repoinfo, err := rs.GetRepoInfo(ctx, repo, user)
+
if err != nil {
+
return err
+
}
+
labelDefs, err := db.GetLabelDefinitions(
+
d,
+
db.FilterIn("at_uri", repo.Labels),
+
db.FilterContains("scope", tangled.RepoIssueNSID),
+
)
+
if err != nil {
+
return err
+
}
+
defs := make(map[string]*models.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
return p.RepoIssues(w, pages.RepoIssuesParams{
+
LoggedInUser: user,
+
RepoInfo: *repoinfo,
+
+
Issues: issues,
+
LabelDefs: defs,
+
FilteringByOpen: searchOpts.IsOpen,
+
FilterQuery: searchOpts.Keyword,
+
Page: searchOpts.Page,
+
})
+
}()
+
if err != nil {
+
l.Error("failed to render", "err", err)
+
p.Error503(w)
+
return
+
}
+
}
+
}
NEW
appview/web/handler/user_repo_issues_issue.go
···
+
package handler
+
+
import (
+
"net/http"
+
+
"tangled.org/core/appview/pages"
+
isvc "tangled.org/core/appview/service/issue"
+
rsvc "tangled.org/core/appview/service/repo"
+
"tangled.org/core/appview/session"
+
"tangled.org/core/appview/web/request"
+
"tangled.org/core/log"
+
)
+
+
func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
ctx := r.Context()
+
l := log.FromContext(ctx).With("handler", "Issue")
+
issue, ok := request.IssueFromContext(ctx)
+
if !ok {
+
l.Error("malformed request, failed to get issue")
+
p.Error503(w)
+
return
+
}
+
+
// render
+
err := func() error {
+
user := session.UserFromContext(ctx)
+
repoinfo, err := rs.GetRepoInfo(ctx, issue.Repo, user)
+
if err != nil {
+
return err
+
}
+
return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{
+
LoggedInUser: user,
+
RepoInfo: *repoinfo,
+
Issue: issue,
+
})
+
}()
+
if err != nil {
+
l.Error("failed to render", "err", err)
+
p.Error503(w)
+
return
+
}
+
}
+
}
+
+
func IssueDelete(s isvc.Service, p *pages.Pages) http.HandlerFunc {
+
noticeId := "issue-actions-error"
+
return func(w http.ResponseWriter, r *http.Request) {
+
ctx := r.Context()
+
l := log.FromContext(ctx).With("handler", "IssueDelete")
+
issue, ok := request.IssueFromContext(ctx)
+
if !ok {
+
l.Error("failed to get issue")
+
// TODO: 503 error with more detailed messages
+
p.Error503(w)
+
return
+
}
+
err := s.DeleteIssue(ctx, issue)
+
if err != nil {
+
p.Notice(w, noticeId, "failed to delete issue")
+
return
+
}
+
p.HxLocation(w, "/")
+
}
+
}
NEW
appview/web/handler/user_repo_issues_issue_close.go
···
+
package handler
+
+
import (
+
"net/http"
+
+
"tangled.org/core/appview/service/issue"
+
)
+
+
func CloseIssue(s issue.Service) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
panic("unimplemented")
+
}
+
}
NEW
appview/web/handler/user_repo_issues_issue_edit.go
···
+
package handler
+
+
import (
+
"errors"
+
"net/http"
+
+
"tangled.org/core/appview/pages"
+
isvc "tangled.org/core/appview/service/issue"
+
rsvc "tangled.org/core/appview/service/repo"
+
"tangled.org/core/appview/session"
+
"tangled.org/core/appview/web/request"
+
"tangled.org/core/log"
+
)
+
+
func IssueEdit(is isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
ctx := r.Context()
+
l := log.FromContext(ctx).With("handler", "IssueEdit")
+
issue, ok := request.IssueFromContext(ctx)
+
if !ok {
+
l.Error("malformed request, failed to get issue")
+
p.Error503(w)
+
return
+
}
+
+
// render
+
err := func() error {
+
user := session.UserFromContext(ctx)
+
repoinfo, err := rs.GetRepoInfo(ctx, issue.Repo, user)
+
if err != nil {
+
return err
+
}
+
return p.EditIssueFragment(w, pages.EditIssueParams{
+
LoggedInUser: user,
+
RepoInfo: *repoinfo,
+
+
Issue: issue,
+
})
+
}()
+
if err != nil {
+
l.Error("failed to render", "err", err)
+
p.Error503(w)
+
return
+
}
+
}
+
}
+
+
func IssueEditPost(is isvc.Service, p *pages.Pages) http.HandlerFunc {
+
noticeId := "issues"
+
return func(w http.ResponseWriter, r *http.Request) {
+
ctx := r.Context()
+
l := log.FromContext(ctx).With("handler", "IssueEdit")
+
issue, ok := request.IssueFromContext(ctx)
+
if !ok {
+
l.Error("malformed request, failed to get issue")
+
p.Error503(w)
+
return
+
}
+
+
newIssue := *issue
+
newIssue.Title = r.FormValue("title")
+
newIssue.Body = r.FormValue("body")
+
+
err := is.EditIssue(ctx, &newIssue)
+
if err != nil {
+
if errors.Is(err, isvc.ErrDatabaseFail) {
+
p.Notice(w, noticeId, "Failed to edit issue.")
+
} else if errors.Is(err, isvc.ErrPDSFail) {
+
p.Notice(w, noticeId, "Failed to edit issue.")
+
} else {
+
p.Notice(w, noticeId, "Failed to edit issue.")
+
}
+
}
+
+
p.HxRefresh(w)
+
}
+
}
NEW
appview/web/handler/user_repo_issues_issue_opengraph.go
···
+
package handler
+
+
import (
+
"net/http"
+
+
"tangled.org/core/appview/service/issue"
+
)
+
+
func IssueOpenGraph(s issue.Service) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
panic("unimplemented")
+
}
+
}
NEW
appview/web/handler/user_repo_issues_issue_reopen.go
···
+
package handler
+
+
import (
+
"net/http"
+
+
"tangled.org/core/appview/service/issue"
+
)
+
+
func ReopenIssue(s issue.Service) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
panic("unimplemented")
+
}
+
}
NEW
appview/web/handler/user_repo_issues_new.go
···
+
package handler
+
+
import (
+
"errors"
+
"fmt"
+
"net/http"
+
+
"tangled.org/core/appview/pages"
+
isvc "tangled.org/core/appview/service/issue"
+
rsvc "tangled.org/core/appview/service/repo"
+
"tangled.org/core/appview/session"
+
"tangled.org/core/appview/web/request"
+
"tangled.org/core/log"
+
)
+
+
func NewIssue(rs rsvc.Service, p *pages.Pages) http.HandlerFunc {
+
return func(w http.ResponseWriter, r *http.Request) {
+
ctx := r.Context()
+
l := log.FromContext(ctx).With("handler", "NewIssue")
+
+
// render
+
err := func() error {
+
user := session.UserFromContext(ctx)
+
repo, ok := request.RepoFromContext(ctx)
+
if !ok {
+
return fmt.Errorf("malformed request")
+
}
+
repoinfo, err := rs.GetRepoInfo(ctx, repo, user)
+
if err != nil {
+
return err
+
}
+
return p.RepoNewIssue(w, pages.RepoNewIssueParams{
+
LoggedInUser: user,
+
RepoInfo: *repoinfo,
+
})
+
}()
+
if err != nil {
+
l.Error("failed to render", "err", err)
+
p.Error503(w)
+
return
+
}
+
}
+
}
+
+
func NewIssuePost(is isvc.Service, p *pages.Pages) http.HandlerFunc {
+
noticeId := "issues"
+
return func(w http.ResponseWriter, r *http.Request) {
+
ctx := r.Context()
+
l := log.FromContext(ctx).With("handler", "NewIssuePost")
+
repo, ok := request.RepoFromContext(ctx)
+
if !ok {
+
l.Error("malformed request, failed to get repo")
+
// TODO: 503 error with more detailed messages
+
p.Error503(w)
+
return
+
}
+
var (
+
title = r.FormValue("title")
+
body = r.FormValue("body")
+
)
+
+
_, err := is.NewIssue(ctx, repo, title, body)
+
if err != nil {
+
if errors.Is(err, isvc.ErrDatabaseFail) {
+
p.Notice(w, noticeId, "Failed to create issue.")
+
} else if errors.Is(err, isvc.ErrPDSFail) {
+
p.Notice(w, noticeId, "Failed to create issue.")
+
} else {
+
p.Notice(w, noticeId, "Failed to create issue.")
+
}
+
}
+
p.HxLocation(w, "/")
+
}
+
}
NEW
appview/web/middleware/ensuredidorhandle.go
···
+
package middleware
+
+
import (
+
"net/http"
+
+
"github.com/go-chi/chi/v5"
+
"tangled.org/core/appview/pages"
+
"tangled.org/core/appview/state/userutil"
+
)
+
+
// EnsureDidOrHandle ensures the "user" url param is valid did/handle format.
+
// If not, respond with 404
+
func EnsureDidOrHandle(p *pages.Pages) middlewareFunc {
+
return func(next http.Handler) http.Handler {
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
user := chi.URLParam(r, "user")
+
+
// if using a DID or handle, just continue as per usual
+
if userutil.IsDid(user) || userutil.IsHandle(user) {
+
next.ServeHTTP(w, r)
+
return
+
}
+
+
// TODO: run Normalize middleware from here
+
+
p.Error404(w)
+
return
+
})
+
}
+
}
NEW
appview/web/middleware/normalize.go
···
+
package middleware
+
+
import (
+
"net/http"
+
"strings"
+
+
"github.com/go-chi/chi/v5"
+
"tangled.org/core/appview/state/userutil"
+
)
+
+
func Normalize() middlewareFunc {
+
return func(next http.Handler) http.Handler {
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
pat := chi.URLParam(r, "*")
+
pathParts := strings.SplitN(pat, "/", 2)
+
if len(pathParts) == 0 {
+
next.ServeHTTP(w, r)
+
return
+
}
+
+
firstPart := pathParts[0]
+
+
// if using a flattened DID (like you would in go modules), unflatten
+
if userutil.IsFlattenedDid(firstPart) {
+
unflattenedDid := userutil.UnflattenDid(firstPart)
+
redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/")
+
+
redirectURL := *r.URL
+
redirectURL.Path = "/" + redirectPath
+
+
http.Redirect(w, r, redirectURL.String(), http.StatusFound)
+
return
+
}
+
+
// if using a handle with @, rewrite to work without @
+
if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) {
+
redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/")
+
+
redirectURL := *r.URL
+
redirectURL.Path = "/" + redirectPath
+
+
http.Redirect(w, r, redirectURL.String(), http.StatusFound)
+
return
+
}
+
+
next.ServeHTTP(w, r)
+
return
+
})
+
}
+
}
NEW
appview/web/request/context.go
···
+
package request
+
+
import (
+
"context"
+
+
"github.com/bluesky-social/indigo/atproto/identity"
+
"tangled.org/core/appview/models"
+
)
+
+
type ctxKeyOwner struct{}
+
type ctxKeyRepo struct{}
+
type ctxKeyIssue struct{}
+
+
func WithOwner(ctx context.Context, owner *identity.Identity) context.Context {
+
return context.WithValue(ctx, ctxKeyOwner{}, owner)
+
}
+
+
func OwnerFromContext(ctx context.Context) (*identity.Identity, bool) {
+
owner, ok := ctx.Value(ctxKeyOwner{}).(*identity.Identity)
+
return owner, ok
+
}
+
+
func WithRepo(ctx context.Context, repo *models.Repo) context.Context {
+
return context.WithValue(ctx, ctxKeyRepo{}, repo)
+
}
+
+
func RepoFromContext(ctx context.Context) (*models.Repo, bool) {
+
repo, ok := ctx.Value(ctxKeyRepo{}).(*models.Repo)
+
return repo, ok
+
}
+
+
func WithIssue(ctx context.Context, issue *models.Issue) context.Context {
+
return context.WithValue(ctx, ctxKeyIssue{}, issue)
+
}
+
+
func IssueFromContext(ctx context.Context) (*models.Issue, bool) {
+
issue, ok := ctx.Value(ctxKeyIssue{}).(*models.Issue)
+
return issue, ok
+
}
NEW
cmd/appview/main.go
···
"tangled.org/core/appview/config"
"tangled.org/core/appview/state"
+
"tangled.org/core/appview/web"
tlog "tangled.org/core/log"
)
···
logger.Info("starting server", "address", c.Core.ListenAddr)
-
if err := http.ListenAndServe(c.Core.ListenAddr, state.Router()); err != nil {
+
if err := http.ListenAndServe(c.Core.ListenAddr, web.RouterFromState(state)); err != nil {
logger.Error("failed to start appview", "err", err)
}
}