Files
lassistanoque/backend/internal/http/handlers/auth.go
T

163 lines
5.0 KiB
Go

package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
)
func NewAuthGroup(prefix string, e *echo.Group, service *auth.Service) *echo.Group {
authHandler := &AuthHandler{
authService: service,
}
auth := e.Group(prefix)
auth.POST("/login", authHandler.Login)
auth.GET("/oidc/providers", authHandler.OIDCProviders)
auth.GET("/oidc/login", authHandler.OIDCLogin)
auth.GET("/oidc/callback", authHandler.OIDCCallback)
auth.POST("/register", authHandler.Register)
return auth
}
type AuthHandler struct {
authService *auth.Service
}
type loginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
type loginResponse struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresAt string `json:"expiresAt"`
User *domain.User `json:"user"`
}
func (h AuthHandler) Login(c *echo.Context) error {
var req loginRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
}
if req.Email == "" || req.Password == "" {
return echo.NewHTTPError(http.StatusBadRequest, "email and password are required")
}
creds := auth.Credentials{
Method: "password",
Email: req.Email,
Password: req.Password,
}
session, err := h.authService.Login(context.Background(), creds)
if err != nil {
c.Logger().Error(fmt.Sprintf("error while login: %s", err))
return echo.NewHTTPError(http.StatusUnauthorized, "login error")
}
resp := loginResponse{
AccessToken: session.AccessToken,
RefreshToken: session.RefreshToken,
ExpiresAt: session.ExpiresAt.Format("2006-01-02T15:04:05Z07:00"),
}
resp.User = session.User
return c.JSON(http.StatusOK, resp)
}
// oidcRedirectURI reconstruit l'URL de callback du serveur à partir de la
// requête. Elle doit être identique entre /oidc/login et /oidc/callback.
func oidcRedirectURI(c *echo.Context) string {
scheme := "http"
if proto := c.Request().Header.Get("X-Forwarded-Proto"); proto != "" {
scheme = proto
} else if c.Request().TLS != nil {
scheme = "https"
}
host := c.Request().Header.Get("X-Forwarded-Host")
if host == "" {
host = c.Request().Host
}
return scheme + "://" + host + "/api/auth/oidc/callback"
}
func (h AuthHandler) OIDCProviders(c *echo.Context) error {
providers, err := h.authService.ListOIDCProviders()
if err != nil {
c.Logger().Error(fmt.Sprintf("error listing oidc providers: %s", err))
return echo.NewHTTPError(http.StatusInternalServerError, "error listing oidc providers")
}
return c.JSON(http.StatusOK, providers)
}
func (h AuthHandler) OIDCLogin(c *echo.Context) error {
providerID := c.QueryParam("provider")
if providerID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "provider query parameter is required")
}
authorizeURL, err := h.authService.OIDCAuthorizeURL(context.Background(), providerID, oidcRedirectURI(c))
if err != nil {
c.Logger().Error(fmt.Sprintf("error building oidc authorize url: %s", err))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
return c.Redirect(http.StatusFound, authorizeURL)
}
func (h AuthHandler) OIDCCallback(c *echo.Context) error {
if errParam := c.QueryParam("error"); errParam != "" {
c.Logger().Error(fmt.Sprintf("oidc provider returned an error: %s", errParam))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
code := c.QueryParam("code")
state := c.QueryParam("state")
if code == "" || state == "" {
return echo.NewHTTPError(http.StatusBadRequest, "code and state are required")
}
session, err := h.authService.OIDCCallback(context.Background(), state, code, oidcRedirectURI(c))
if err != nil {
c.Logger().Error(fmt.Sprintf("error while oidc callback: %s", err))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
// L'application SPA est servie par ce même serveur (même origine) et lit
// ses tokens dans localStorage: on les dépose ici puis on redirige vers
// l'application, sans jamais les faire transiter dans une URL.
page := fmt.Sprintf(`<!doctype html>
<html>
<head><meta charset="utf-8"><title>Connexion en cours...</title></head>
<body>
<script>
localStorage.setItem("accessToken", %q);
localStorage.setItem("refreshToken", %q);
location.replace("/");
</script>
</body>
</html>`, session.AccessToken, session.RefreshToken)
return c.Blob(http.StatusOK, "text/html; charset=utf-8", []byte(page))
}
func (h AuthHandler) Register(c *echo.Context) error {
reader := c.Request().Body
var registration auth.Registration
decoder := json.NewDecoder(reader)
err := decoder.Decode(&registration)
if err != nil {
return c.String(http.StatusBadRequest, "malformatted registration")
}
user, err := h.authService.Register(registration)
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusCreated, user)
}