package auth import ( "errors" "net/http" "strings" "time" "mira/backend/internal/users" "github.com/gin-gonic/gin" ) const refreshCookieName = "mira_refresh_token" type Routes struct { users users.Store sessions SessionStore tokens TokenService refreshTTL time.Duration secureCookies bool loginRateLimiter *LoginRateLimiter } func NewRoutes(users users.Store, sessions SessionStore, tokens TokenService, refreshTTL time.Duration, secureCookies bool) Routes { return Routes{ users: users, sessions: sessions, tokens: tokens, refreshTTL: refreshTTL, secureCookies: secureCookies, loginRateLimiter: NewLoginRateLimiter(5, 15*time.Minute), } } func (r Routes) Register(router *gin.RouterGroup) { router.POST("/login", r.login) router.POST("/invitations/accept", r.acceptInvitation) router.POST("/logout", r.logout) router.POST("/refresh", r.refresh) } type loginRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` } func (r Routes) login(c *gin.Context) { var input loginRequest if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"message": "Informe e-mail e senha validos."}) return } email := strings.TrimSpace(input.Email) rateLimitKey := LoginRateLimitKey(c.ClientIP(), email) if r.loginRateLimiter.IsBlocked(rateLimitKey) { c.JSON(http.StatusTooManyRequests, gin.H{"message": "Muitas tentativas de login. Tente novamente em alguns minutos."}) return } user, err := r.users.FindByEmail(c.Request.Context(), email) if errors.Is(err, users.ErrNotFound) { r.loginRateLimiter.RecordFailure(rateLimitKey) c.JSON(http.StatusUnauthorized, gin.H{"message": "E-mail ou senha invalidos."}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "Nao foi possivel fazer login."}) return } if !CheckPassword(input.Password, user.PasswordHash) || user.Status != "active" { r.loginRateLimiter.RecordFailure(rateLimitKey) c.JSON(http.StatusUnauthorized, gin.H{"message": "E-mail ou senha invalidos."}) return } token, err := r.createSession(c, user) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "Nao foi possivel criar a sessao."}) return } r.loginRateLimiter.Clear(rateLimitKey) c.JSON(http.StatusOK, gin.H{ "access_token": token, "token_type": "Bearer", "user": gin.H{ "id": user.ID, "email": user.Email, "name": user.Name, "role": user.Role, "status": user.Status, }, }) } type acceptInvitationRequest struct { Token string `json:"token" binding:"required"` Name string `json:"name" binding:"required,min=2"` Password string `json:"password" binding:"required,min=12"` } func (r Routes) acceptInvitation(c *gin.Context) { var input acceptInvitationRequest if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"message": "Informe nome, senha e token validos."}) return } passwordHash, err := HashPassword(input.Password) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "Nao foi possivel proteger a senha."}) return } user, err := r.users.AcceptInvitation( c.Request.Context(), users.HashInvitationToken(strings.TrimSpace(input.Token)), strings.TrimSpace(input.Name), passwordHash, ) if errors.Is(err, users.ErrInvitationNotFound) { c.JSON(http.StatusBadRequest, gin.H{"message": "Convite invalido ou expirado."}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "Nao foi possivel aceitar o convite."}) return } c.JSON(http.StatusOK, gin.H{ "message": "Senha criada com sucesso. Faca login para acessar o Mira.", "user": gin.H{ "id": user.ID, "email": user.Email, "name": user.Name, "role": user.Role, "status": user.Status, }, }) } func (r Routes) refresh(c *gin.Context) { refreshToken, err := c.Cookie(refreshCookieName) if err != nil || strings.TrimSpace(refreshToken) == "" { c.JSON(http.StatusUnauthorized, gin.H{"message": "Sessao expirada."}) return } tokenHash := HashRefreshToken(refreshToken) session, err := r.sessions.FindValidRefreshSession(c.Request.Context(), tokenHash) if errors.Is(err, ErrRefreshTokenNotFound) { r.clearRefreshCookie(c) c.JSON(http.StatusUnauthorized, gin.H{"message": "Sessao expirada."}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "Nao foi possivel renovar a sessao."}) return } if err := r.sessions.RevokeRefreshSession(c.Request.Context(), tokenHash); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "Nao foi possivel renovar a sessao."}) return } accessToken, err := r.createSession(c, session.User) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "Nao foi possivel renovar a sessao."}) return } c.JSON(http.StatusOK, gin.H{ "access_token": accessToken, "token_type": "Bearer", "user": gin.H{ "id": session.User.ID, "email": session.User.Email, "name": session.User.Name, "role": session.User.Role, "status": session.User.Status, }, }) } func (r Routes) logout(c *gin.Context) { refreshToken, err := c.Cookie(refreshCookieName) if err == nil && strings.TrimSpace(refreshToken) != "" { _ = r.sessions.RevokeRefreshSession(c.Request.Context(), HashRefreshToken(refreshToken)) } r.clearRefreshCookie(c) c.JSON(http.StatusOK, gin.H{"message": "Sessao encerrada."}) } func (r Routes) createSession(c *gin.Context, user users.User) (string, error) { refreshToken, err := GenerateRefreshToken() if err != nil { return "", err } if err := r.sessions.CreateRefreshSession( c.Request.Context(), user.ID, HashRefreshToken(refreshToken), time.Now().Add(r.refreshTTL), ); err != nil { return "", err } r.setRefreshCookie(c, refreshToken) return r.tokens.IssueAccessToken(user.ID, user.Email, user.Role) } func (r Routes) setRefreshCookie(c *gin.Context, token string) { c.SetSameSite(http.SameSiteLaxMode) c.SetCookie(refreshCookieName, token, int(r.refreshTTL.Seconds()), "/api/v1/auth", "", r.secureCookies, true) } func (r Routes) clearRefreshCookie(c *gin.Context) { c.SetSameSite(http.SameSiteLaxMode) c.SetCookie(refreshCookieName, "", 0, "/api/v1/auth", "", r.secureCookies, true) }