package httpapi import ( "errors" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/szabadpalya/api/internal/auth" ) type magicLinkRequest struct { Email string `json:"email"` Callback string `json:"callback"` } // requestMagicLink always reports success, so the endpoint cannot be used to // discover which addresses have accounts. func (s *Server) requestMagicLink(c *gin.Context) { var req magicLinkRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid-email"}) return } email := strings.ToLower(strings.TrimSpace(req.Email)) if !strings.Contains(email, "@") || strings.HasPrefix(email, "@") || strings.HasSuffix(email, "@") { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid-email"}) return } // Only ever redirect within our own site: a caller-supplied callback is an // open-redirect waiting to happen. callback := req.Callback if !strings.HasPrefix(callback, "/") || strings.HasPrefix(callback, "//") { callback = "/admin" } if err := s.auth.SendMagicLink(c.Request.Context(), email, callback); err != nil { // Swallowed on purpose: a failure here would otherwise reveal whether // the address exists. The link either arrives or it does not. c.Error(err) } c.JSON(http.StatusOK, gin.H{"sent": true}) } // verifyMagicLink is a top-level navigation from the user's mail client, so it // answers with a redirect rather than JSON. func (s *Server) verifyMagicLink(c *gin.Context) { callback := c.Query("callback") if !strings.HasPrefix(callback, "/") || strings.HasPrefix(callback, "//") { callback = "/admin" } cookie, err := s.auth.Verify(c.Request.Context(), c.Query("token"), c.ClientIP(), c.Request.UserAgent()) if errors.Is(err, auth.ErrInvalidToken) { c.Redirect(http.StatusFound, "/belepes?error=lejart") return } if err != nil { c.Error(err) c.Redirect(http.StatusFound, "/belepes?error=hiba") return } http.SetCookie(c.Writer, cookie) c.Redirect(http.StatusFound, callback) } func (s *Server) logout(c *gin.Context) { var token string if cookie, err := c.Request.Cookie(auth.SessionCookie); err == nil { token = cookie.Value } http.SetCookie(c.Writer, s.auth.Logout(c.Request.Context(), token)) c.JSON(http.StatusOK, gin.H{"ok": true}) } // session is what the frontend asks on load to decide whether to show admin // affordances. Anonymous is a normal answer, not an error. func (s *Server) session(c *gin.Context) { c.Header("Cache-Control", "no-store") user := s.auth.UserFromRequest(c.Request.Context(), c.Request) if user == nil { c.JSON(http.StatusOK, gin.H{"user": nil, "isAdmin": false}) return } c.JSON(http.StatusOK, gin.H{ "user": gin.H{"email": user.Email, "name": user.Name}, "isAdmin": s.auth.IsAdmin(user), }) }