diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index 29d7bfd..b5983fd 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -1,10 +1,16 @@ package auth import ( + "time" + "github.com/eduardolat/pgbackweb/internal/config" "github.com/eduardolat/pgbackweb/internal/database/dbgen" ) +const ( + maxSessionAge = time.Hour * 12 +) + type Service struct { env *config.Env dbgen *dbgen.Queries diff --git a/internal/service/auth/cookies.go b/internal/service/auth/cookies.go new file mode 100644 index 0000000..20ce50d --- /dev/null +++ b/internal/service/auth/cookies.go @@ -0,0 +1,23 @@ +package auth + +import ( + "net/http" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/labstack/echo/v4" +) + +const ( + sessionCookieName = "pbw_session" +) + +func (s *Service) SetSessionCookie(c echo.Context, session dbgen.Session) { + cookie := http.Cookie{ + Name: sessionCookieName, + Value: session.Token, + MaxAge: int(maxSessionAge.Seconds()), + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + } + c.SetCookie(&cookie) +} diff --git a/internal/service/auth/delete_old_sessions.go b/internal/service/auth/delete_old_sessions.go index 09ecbef..2b5add5 100644 --- a/internal/service/auth/delete_old_sessions.go +++ b/internal/service/auth/delete_old_sessions.go @@ -7,8 +7,6 @@ import ( "github.com/eduardolat/pgbackweb/internal/logger" ) -const maxSessionAge = time.Hour * 12 - func (s *Service) DeleteOldSessions() { ctx := context.Background() dateThreshold := time.Now().Add(-maxSessionAge) diff --git a/internal/view/web/auth/login.go b/internal/view/web/auth/login.go index c5cb6d6..854819a 100644 --- a/internal/view/web/auth/login.go +++ b/internal/view/web/auth/login.go @@ -4,8 +4,11 @@ import ( "net/http" lucide "github.com/eduardolat/gomponents-lucide" + "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" + "github.com/eduardolat/pgbackweb/internal/view/web/htmx" "github.com/eduardolat/pgbackweb/internal/view/web/layout" "github.com/labstack/echo/v4" "github.com/maragudk/gomponents" @@ -21,6 +24,8 @@ func loginPage() gomponents.Node { component.H1Text("Login"), html.Form( + htmx.HxPost("/auth/login"), + htmx.HxDisabledELT("find button"), html.Class("mt-4 space-y-2"), component.InputControl(component.InputControlParams{ @@ -42,7 +47,8 @@ func loginPage() gomponents.Node { }), html.Div( - html.Class("pt-2 grid place-items-end"), + html.Class("pt-2 flex justify-end items-center space-x-2"), + component.HxLoadingMd(), html.Button( html.Class("btn btn-primary"), html.Type("submit"), @@ -58,3 +64,34 @@ func loginPage() gomponents.Node { Body: content, }) } + +func (h *handlers) loginHandler(c echo.Context) error { + ctx := c.Request().Context() + + var formData struct { + Email string `form:"email" validate:"required,email"` + Password string `form:"password" validate:"required,max=50"` + } + if err := c.Bind(&formData); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + if err := validate.Struct(&formData); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + session, err := h.servs.AuthService.Login( + ctx, formData.Email, formData.Password, c.RealIP(), c.Request().UserAgent(), + ) + if err != nil { + logger.Error("login failed", logger.KV{ + "email": formData.Email, + "ip": c.RealIP(), + "ua": c.Request().UserAgent(), + "err": err, + }) + return htmx.RespondToastError(c, "Login failed") + } + + h.servs.AuthService.SetSessionCookie(c, session) + return htmx.RespondRedirect(c, "/dashboard") +} diff --git a/internal/view/web/auth/router.go b/internal/view/web/auth/router.go index 8bd0680..6eafd53 100644 --- a/internal/view/web/auth/router.go +++ b/internal/view/web/auth/router.go @@ -16,4 +16,5 @@ func MountRouter(parent *echo.Group, servs *service.Service) { parent.POST("/create-first-user", h.createFirstUserHandler) parent.GET("/login", h.loginPageHandler) + parent.POST("/login", h.loginHandler) }