szabadpalya/api/internal/httpapi/auth.go

94 lines
2.7 KiB
Go

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),
})
}