Wire git smart-HTTP routes with basic auth
DDavid committed on 2026-06-28 01:05 commit 324327f
cmd/server/main.go +7 −1
package main
import (
"log"
"net/http"
"gitgud/internal/app"
"gitgud/internal/infra/config"
"gitgud/internal/infra/git"
"gitgud/internal/infra/persistence/sqlite"
"gitgud/internal/infra/security"
"gitgud/internal/infra/session"
"gitgud/internal/interface/web"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
db, err := sqlite.Open(cfg.DBPath())
if err != nil {
log.Fatal(err)
}
defer db.Close()
hasher := security.NewBcryptHasher()
userRepo := sqlite.NewUserRepo(db)
userService := app.NewUserService(userRepo, hasher)
gitSvc := git.NewCLIGit(cfg.ReposDir())
repoRepo := sqlite.NewRepoRepo(db)
repoService := app.NewRepoService(repoRepo, gitSvc)
+ gitAccess := app.NewGitAccessService(repoRepo)
+
+ gitBackend, err := git.NewBackend(cfg.ReposDir())
+ if err != nil {
+ log.Fatal(err)
+ }
sm := session.NewSessionManager(db)
- handlers := web.NewHandlers(userService, repoService, sm)
+ handlers := web.NewHandlers(userService, repoService, gitAccess, gitBackend, sm)
handler := web.NewRouter(cfg, handlers)
log.Printf("listening on %s", cfg.Addr)
log.Fatal(http.ListenAndServe(cfg.Addr, handler))
}
internal/interface/web/git_handler.go +58 −0
+package web
+
+import (
+ "errors"
+ "net/http"
+ "strings"
+
+ "github.com/go-chi/chi"
+
+ "gitgud/internal/domain"
+)
+
+func (h *Handlers) gitHTTP(w http.ResponseWriter, r *http.Request) {
+ owner := chi.URLParam(r, "owner")
+ repoName := strings.TrimSuffix(chi.URLParam(r, "repo"), ".git")
+
+ isPush := strings.HasSuffix(r.URL.Path, "git-receive-pack") ||
+ r.URL.Query().Get("service") == "git-receive-pack"
+
+ user := h.basicAuthUser(r)
+
+ allow, needAuth, remoteUser, err := h.gitAccess.Authorize(r.Context(), owner, repoName, isPush, user)
+ if needAuth {
+ requireBasicAuth(w)
+ return
+ }
+ if err != nil {
+ if errors.Is(err, domain.ErrPermission) {
+ http.Error(w, "forbidden", http.StatusForbidden)
+ return
+ }
+ http.NotFound(w, r)
+ return
+ }
+ if !allow {
+ http.NotFound(w, r)
+ return
+ }
+
+ h.gitBackend.Handler(remoteUser).ServeHTTP(w, r)
+}
+
+func (h *Handlers) basicAuthUser(r *http.Request) *domain.User {
+ username, password, ok := r.BasicAuth()
+ if !ok {
+ return nil
+ }
+ u, err := h.users.Authenticate(r.Context(), username, password)
+ if err != nil {
+ return nil
+ }
+ return u
+}
+
+func requireBasicAuth(w http.ResponseWriter) {
+ w.Header().Set("WWW-Authenticate", `Basic realm="gitgud"`)
+ http.Error(w, "authentication required", http.StatusUnauthorized)
+}
internal/interface/web/middleware.go +8 −5
package web
import (
"context"
"net/http"
"strconv"
"github.com/alexedwards/scs/v2"
"gitgud/internal/app"
"gitgud/internal/domain"
+ "gitgud/internal/infra/git"
)
const sessionUserIDKey = "user_id"
type ctxKey int
const userCtxKey ctxKey = iota
type Handlers struct {
- users *app.UserService
- repos *app.RepoService
- sm *scs.SessionManager
+ users *app.UserService
+ repos *app.RepoService
+ gitAccess *app.GitAccessService
+ gitBackend *git.Backend
+ sm *scs.SessionManager
}
-func NewHandlers(users *app.UserService, repos *app.RepoService, sm *scs.SessionManager) *Handlers {
- return &Handlers{users: users, repos: repos, sm: sm}
+func NewHandlers(users *app.UserService, repos *app.RepoService, gitAccess *app.GitAccessService, gitBackend *git.Backend, sm *scs.SessionManager) *Handlers {
+ return &Handlers{users: users, repos: repos, gitAccess: gitAccess, gitBackend: gitBackend, sm: sm}
}
func (h *Handlers) withUser(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if id := h.sm.GetInt64(r.Context(), sessionUserIDKey); id != 0 {
if u, err := h.users.ByID(r.Context(), strconv.FormatInt(id, 10)); err == nil {
r = r.WithContext(context.WithValue(r.Context(), userCtxKey, u))
}
}
next.ServeHTTP(w, r)
})
}
func (h *Handlers) requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if currentUser(r.Context()) == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func currentUser(ctx context.Context) *domain.User {
u, _ := ctx.Value(userCtxKey).(*domain.User)
return u
}
internal/interface/web/router.go +4 −0
package web
import (
"embed"
"net/http"
"github.com/a-h/templ"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"gitgud/internal/infra/config"
)
var staticFS embed.FS
func NewRouter(cfg config.Config, h *Handlers) http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger, middleware.Recoverer)
r.Use(h.sm.LoadAndSave)
r.Use(h.withUser)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
r.Handle("/static/*", http.StripPrefix("/static", http.FileServer((http.FS(staticFS)))))
r.Get("/register", h.showRegister)
r.Post("/register", h.doRegister)
r.Get("/login", h.showLogin)
r.Post("/login", h.doLogin)
r.Post("/logout", h.doLogout)
r.With(h.requireAuth).Get("/new", h.showNewRepo)
r.With(h.requireAuth).Post("/new", h.createRepo)
+ r.Get("/{owner}/{repo}/info/refs", h.gitHTTP)
+ r.Post("/{owner}/{repo}/git-upload-pack", h.gitHTTP)
+ r.Post("/{owner}/{repo}/git-receive-pack", h.gitHTTP)
+
r.Get("/", h.dashboard)
r.Get("/{owner}", h.profile)
r.Get("/{owner}/{repo}", h.repoHome)
return r
}
func render(w http.ResponseWriter, r *http.Request, status int, c templ.Component) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
if err := c.Render(r.Context(), w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}