ikurotime / gitgud

public
Wire auth handlers, session middleware and routes
DDavid committed on 2026-06-28 00:45 commit 6dea0f5
cmd/server/main.go +13 −3
package main
import (
+ "log"
+ "net/http"
+
+ "gitgud/internal/app"
"gitgud/internal/infra/config"
"gitgud/internal/infra/persistence/sqlite"
+ "gitgud/internal/infra/security"
+ "gitgud/internal/infra/session"
"gitgud/internal/interface/web"
- "log"
- "net/http"
)
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()
- handler := web.NewRouter(cfg)
+ hasher := security.NewBcryptHasher()
+ userRepo := sqlite.NewUserRepo(db)
+ userService := app.NewUserService(userRepo, hasher)
+ sm := session.NewSessionManager(db)
+ handlers := web.NewHandlers(userService, sm)
+
+ handler := web.NewRouter(cfg, handlers)
log.Printf("listening on %s", cfg.Addr)
log.Fatal(http.ListenAndServe(cfg.Addr, handler))
}
internal/interface/web/auth_handler.go +74 −0
+package web
+
+import (
+ "errors"
+ "net/http"
+ "strings"
+
+ "gitgud/internal/domain"
+ "gitgud/internal/interface/web/templates"
+)
+
+func (h *Handlers) showRegister(w http.ResponseWriter, r *http.Request) {
+ render(w, r, http.StatusOK, templates.Register(currentUser(r.Context()), "", "", ""))
+}
+
+func (h *Handlers) doRegister(w http.ResponseWriter, r *http.Request) {
+ username := r.FormValue("username")
+ email := r.FormValue("email")
+ password := r.FormValue("password")
+
+ u, err := h.users.Register(r.Context(), username, email, password)
+ if err != nil {
+ status := http.StatusBadRequest
+ if errors.Is(err, domain.ErrConflict) {
+ status = http.StatusConflict
+ }
+ render(w, r, status, templates.Register(currentUser(r.Context()), username, email, userMessage(err)))
+ return
+ }
+ h.startSession(w, r, u)
+}
+
+func (h *Handlers) showLogin(w http.ResponseWriter, r *http.Request) {
+ render(w, r, http.StatusOK, templates.Login(currentUser(r.Context()), "", ""))
+}
+
+func (h *Handlers) doLogin(w http.ResponseWriter, r *http.Request) {
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+
+ u, err := h.users.Authenticate(r.Context(), username, password)
+ if err != nil {
+ render(w, r, http.StatusUnauthorized, templates.Login(currentUser(r.Context()), username, "invalid credentials"))
+ return
+ }
+ h.startSession(w, r, u)
+}
+
+func (h *Handlers) doLogout(w http.ResponseWriter, r *http.Request) {
+ if err := h.sm.Destroy(r.Context()); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+}
+
+func (h *Handlers) startSession(w http.ResponseWriter, r *http.Request, u *domain.User) {
+ h.sm.Put(r.Context(), sessionUserIDKey, u.ID)
+ h.sm.RenewToken(r.Context())
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+}
+
+func userMessage(err error) string {
+ switch {
+ case errors.Is(err, domain.ErrUnauthorized):
+ return "invalid credentials"
+ case errors.Is(err, domain.ErrConflict):
+ return "that username is already taken"
+ case errors.Is(err, domain.ErrValidation):
+ return strings.TrimSuffix(err.Error(), ": "+domain.ErrValidation.Error())
+ default:
+ return "something went wrong"
+ }
+}
internal/interface/web/middleware.go +53 −0
+package web
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+
+ "github.com/alexedwards/scs/v2"
+
+ "gitgud/internal/app"
+ "gitgud/internal/domain"
+)
+
+const sessionUserIDKey = "user_id"
+
+type ctxKey int
+
+const userCtxKey ctxKey = iota
+
+type Handlers struct {
+ users *app.UserService
+ sm *scs.SessionManager
+}
+
+func NewHandlers(users *app.UserService, sm *scs.SessionManager) *Handlers {
+ return &Handlers{users: users, 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 +23 −8
package web
import (
- "context"
"embed"
- "gitgud/internal/infra/config"
- "gitgud/internal/interface/web/templates"
"net/http"
+ "github.com/a-h/templ"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
-)
-var templatesFS embed.FS
+ "gitgud/internal/infra/config"
+ "gitgud/internal/interface/web/templates"
+)
var staticFS embed.FS
-func NewRouter(cfg config.Config) http.Handler {
+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("/", homeHandler)
+
+ 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)
+
return r
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
- component := templates.Home()
- component.Render(context.Background(), w)
+ render(w, r, http.StatusOK, templates.Home(currentUser(r.Context())))
+}
+
+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)
+ }
}