package handler import ( "billit/internal/logic" "billit/internal/view" "net/http" "net/url" "os" "strings" "time" "github.com/labstack/echo/v4" ) // AuthHandlers holds auth service reference type AuthHandlers struct { auth *logic.AuthService } // NewAuthHandlers creates handlers with auth service func NewAuthHandlers(authService *logic.AuthService) *AuthHandlers { return &AuthHandlers{auth: authService} } // createAuthCookie creates an HTTP-only secure cookie for the token func createAuthCookie(token string) *http.Cookie { domain := os.Getenv("COOKIE_DOMAIN") secure := os.Getenv("COOKIE_SECURE") == "true" return &http.Cookie{ Name: "auth_token", Value: token, Path: "/", Domain: domain, MaxAge: int(24 * time.Hour.Seconds()), // Match token duration HttpOnly: true, // Prevents JavaScript access Secure: secure, // Only send over HTTPS in production SameSite: http.SameSiteStrictMode, } } // clearAuthCookie returns a cookie that clears the auth token func clearAuthCookie() *http.Cookie { domain := os.Getenv("COOKIE_DOMAIN") secure := os.Getenv("COOKIE_SECURE") == "true" return &http.Cookie{ Name: "auth_token", Value: "", Path: "/", Domain: domain, MaxAge: -1, HttpOnly: true, Secure: secure, SameSite: http.SameSiteStrictMode, } } // LoginPageHandler renders the login page (home page) func (h *AuthHandlers) LoginPageHandler(c echo.Context) error { // Check if already logged in cookie, err := c.Cookie("auth_token") if err == nil && cookie.Value != "" { _, err := h.auth.ValidateToken(cookie.Value) if err == nil { // Already logged in, redirect to home return c.Redirect(http.StatusFound, "/home") } } // Capture redirect URL from query param redirectURL := c.QueryParam("redirect") return view.Render(c, view.LoginPage("", "", redirectURL)) } // LoginHandler handles login form submission func (h *AuthHandlers) LoginHandler(c echo.Context) error { email := strings.TrimSpace(c.FormValue("email")) password := c.FormValue("password") redirectURL := c.FormValue("redirect") if email == "" || password == "" { return view.Render(c, view.LoginPage("Email and password are required", email, redirectURL)) } token, err := h.auth.Login(email, password) if err != nil { return view.Render(c, view.LoginPage("Invalid email or password", email, redirectURL)) } // Set HTTP-only cookie cookie := createAuthCookie(token) c.SetCookie(cookie) // Redirect to original URL or home page if redirectURL != "" && strings.HasPrefix(redirectURL, "/") { return c.Redirect(http.StatusFound, redirectURL) } return c.Redirect(http.StatusFound, "/home") } // RegisterPageHandler renders the registration page func (h *AuthHandlers) RegisterPageHandler(c echo.Context) error { return view.Render(c, view.RegisterPage("", "")) } // RegisterHandler handles registration form submission func (h *AuthHandlers) RegisterHandler(c echo.Context) error { email := strings.TrimSpace(c.FormValue("email")) password := c.FormValue("password") confirmPassword := c.FormValue("confirm_password") if email == "" || password == "" { return view.Render(c, view.RegisterPage("Email and password are required", email)) } if password != confirmPassword { return view.Render(c, view.RegisterPage("Passwords do not match", email)) } if len(password) < 8 { return view.Render(c, view.RegisterPage("Password must be at least 8 characters", email)) } _, err := h.auth.Register(email, password) if err != nil { if err == logic.ErrUserExists { return view.Render(c, view.RegisterPage("An account with this email already exists", email)) } return view.Render(c, view.RegisterPage(err.Error(), email)) } // Auto-login after registration token, err := h.auth.Login(email, password) if err != nil { return c.Redirect(http.StatusFound, "/") } cookie := createAuthCookie(token) c.SetCookie(cookie) return c.Redirect(http.StatusFound, "/home") } // LogoutHandler clears the auth cookie and redirects to login func (h *AuthHandlers) LogoutHandler(c echo.Context) error { cookie := clearAuthCookie() c.SetCookie(cookie) return c.Redirect(http.StatusFound, "/") } // AuthMiddleware protects routes that require authentication func (h *AuthHandlers) AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { cookie, err := c.Cookie("auth_token") if err != nil || cookie.Value == "" { // No cookie - redirect to login with original URL for post-login redirect redirectPath := url.QueryEscape(c.Request().URL.RequestURI()) return c.Redirect(http.StatusFound, "/?redirect="+redirectPath) } claims, err := h.auth.ValidateToken(cookie.Value) if err != nil { // Invalid/expired token - show session expired dialog c.SetCookie(clearAuthCookie()) redirectPath := url.QueryEscape(c.Request().URL.RequestURI()) return view.Render(c, view.SessionExpiredPage(redirectPath)) } // Store user info in context c.Set("user_id", claims.UserID) c.Set("user_email", claims.Email) return next(c) } }