Files
omnex/control-plane/api/internal/auth/handler.go
T
Nuxgrid 6b4274d12b
docker-publish / publish (push) Failing after 14m14s
feat: add api
2026-07-27 16:06:19 +02:00

177 lines
4.8 KiB
Go

package auth
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/omnex/control-plane/api/internal/session"
)
func (User) TableName() string { return "users" }
type UserStore interface {
ByUsername(username string) (User, bool)
Create(username, passwordHash string, role Role) (User, error)
}
type Handler struct {
users UserStore
sessions session.Manager
iss *Issuer
secure bool // cookie Secure (activé en prod/HTTPS)
}
func NewHandler(users UserStore, sessions session.Manager, iss *Issuer, secure bool) *Handler {
return &Handler{users: users, sessions: sessions, iss: iss, secure: secure}
}
type loginRequest struct {
Username string
Password string
}
// Login authentifie, ouvre une session Redis et pose le cookie httpOnly.
// Réponse volontairement uniforme (pas d'énumération d'utilisateurs).
func (h *Handler) Login(c *gin.Context) {
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
return
}
user, found := h.users.ByUsername(req.Username)
if !found {
// On hache quand même une valeur bidon pour égaliser le temps de réponse.
_, _ = VerifyPassword(req.Password, dummyHash)
c.JSON(http.StatusUnauthorized, gin.H{"error": "identifiants invalides"})
return
}
ok, err := VerifyPassword(req.Password, user.PasswordHash)
if err != nil || !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "identifiants invalides"})
return
}
token, err := h.startSession(c, user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token, "token_type": "Bearer", "role": user.Role})
}
// Me renvoie l'identité de la session courante (dont le rôle, pour le front).
func (h *Handler) Me(c *gin.Context) {
p := PrincipalFrom(c)
if p == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
return
}
user, found := h.users.ByUsername(p.Username)
if !found {
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
return
}
typeAbo := user.TypeAbo
if typeAbo == "premium" && time.Now().UTC().After(user.ExpiredAt) {
typeAbo = "demo" // expiré : on ne le renvoie plus comme premium
}
c.JSON(http.StatusOK, gin.H{
"user_id": p.UserID,
"username": p.Username,
"role": p.Role,
"type_abonnement": typeAbo,
"expired_at": user.ExpiredAt,
})
}
type registerRequest struct {
Username string
Password string
}
// Register crée un compte commercial puis ouvre directement la session.
func (h *Handler) Register(c *gin.Context) {
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
return
}
if _, exists := h.users.ByUsername(req.Username); exists {
c.JSON(http.StatusConflict, gin.H{"error": "nom d'utilisateur déjà pris"})
return
}
hash, err := HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
return
}
user, err := h.users.Create(req.Username, hash, RoleClient)
if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "nom d'utilisateur déjà pris"})
return
}
token, err := h.startSession(c, user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
return
}
c.JSON(http.StatusCreated, gin.H{"token": token, "token_type": "Bearer", "role": user.Role})
}
// startSession ouvre une session Redis, signe le JWT et pose le cookie.
func (h *Handler) startSession(c *gin.Context, user User) (string, error) {
sid, err := h.sessions.Create(c.Request.Context(), session.Session{
UserID: user.ID,
Role: string(user.Role),
})
if err != nil {
return "", err
}
token, err := h.iss.Issue(user.ID, user.Username, user.Role, sid)
if err != nil {
_ = h.sessions.Delete(c.Request.Context(), sid) // fail-secure
return "", err
}
h.setSessionCookie(c, token, int(h.sessions.TTL().Seconds()))
return token, nil
}
// Logout révoque la session Redis courante et efface le cookie.
func (h *Handler) Logout(c *gin.Context) {
if p := PrincipalFrom(c); p != nil {
if err := h.sessions.Delete(c.Request.Context(), p.SessionID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
return
}
}
h.setSessionCookie(c, "", -1)
c.JSON(http.StatusOK, gin.H{"status": "déconnecté"})
}
func (h *Handler) setSessionCookie(c *gin.Context, value string, maxAge int) {
http.SetCookie(c.Writer, &http.Cookie{
Name: session.CookieName,
Value: value,
Path: "/",
MaxAge: maxAge,
HttpOnly: true,
Secure: h.secure,
SameSite: http.SameSiteLaxMode,
})
}
// dummyHash : argon2id d'une valeur arbitraire, pour la mitigation de timing.
const dummyHash = "=19=65536,t=1,p=4$" +
"3Vk0m5m8c1m6l0k9j8h7g6f5d4s3a2z1x0c9v8b7n6m"