Revert "Build post chat and attachment workflows"

This reverts commit 0b920da187.
This commit is contained in:
Cauê Faleiros
2026-06-09 15:57:45 -03:00
parent 0b920da187
commit 5bc4a551af
30 changed files with 204 additions and 3021 deletions

View File

@@ -1,68 +0,0 @@
package auth
import (
"strings"
"sync"
"time"
)
type LoginRateLimiter struct {
mu sync.Mutex
attempts map[string]loginAttempt
limit int
window time.Duration
}
type loginAttempt struct {
Count int
ExpiresAt time.Time
}
func NewLoginRateLimiter(limit int, window time.Duration) *LoginRateLimiter {
return &LoginRateLimiter{
attempts: map[string]loginAttempt{},
limit: limit,
window: window,
}
}
func (l *LoginRateLimiter) IsBlocked(key string) bool {
l.mu.Lock()
defer l.mu.Unlock()
attempt, ok := l.attempts[key]
if !ok || time.Now().After(attempt.ExpiresAt) {
delete(l.attempts, key)
return false
}
return attempt.Count >= l.limit
}
func (l *LoginRateLimiter) RecordFailure(key string) {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
attempt, ok := l.attempts[key]
if !ok || now.After(attempt.ExpiresAt) {
l.attempts[key] = loginAttempt{
Count: 1,
ExpiresAt: now.Add(l.window),
}
return
}
attempt.Count++
l.attempts[key] = attempt
}
func (l *LoginRateLimiter) Clear(key string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.attempts, key)
}
func LoginRateLimitKey(ip string, email string) string {
return strings.TrimSpace(ip) + ":" + strings.ToLower(strings.TrimSpace(email))
}

View File

@@ -4,40 +4,35 @@ 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
users users.Store
tokens TokenService
}
func NewRoutes(users users.Store, sessions SessionStore, tokens TokenService, refreshTTL time.Duration, secureCookies bool) Routes {
func NewRoutes(users users.Store, tokens TokenService) Routes {
return Routes{
users: users,
sessions: sessions,
tokens: tokens,
refreshTTL: refreshTTL,
secureCookies: secureCookies,
loginRateLimiter: NewLoginRateLimiter(5, 15*time.Minute),
users: users,
tokens: tokens,
}
}
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)
router.POST("/logout", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Sessao encerrada."})
})
router.POST("/refresh", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "Renovacao de sessao ainda nao implementada."})
})
}
type loginRequest struct {
@@ -52,16 +47,8 @@ func (r Routes) login(c *gin.Context) {
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)
user, err := r.users.FindByEmail(c.Request.Context(), strings.TrimSpace(input.Email))
if errors.Is(err, users.ErrNotFound) {
r.loginRateLimiter.RecordFailure(rateLimitKey)
c.JSON(http.StatusUnauthorized, gin.H{"message": "E-mail ou senha invalidos."})
return
}
@@ -70,17 +57,15 @@ func (r Routes) login(c *gin.Context) {
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)
token, err := r.tokens.IssueAccessToken(user.ID, user.Email, user.Role)
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,
@@ -129,8 +114,15 @@ func (r Routes) acceptInvitation(c *gin.Context) {
return
}
token, err := r.tokens.IssueAccessToken(user.ID, user.Email, user.Role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "Nao foi possivel criar a sessao."})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Senha criada com sucesso. Faca login para acessar o Mira.",
"access_token": token,
"token_type": "Bearer",
"user": gin.H{
"id": user.ID,
"email": user.Email,
@@ -140,84 +132,3 @@ func (r Routes) acceptInvitation(c *gin.Context) {
},
})
}
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)
}

View File

@@ -1,112 +0,0 @@
package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"time"
"mira/backend/internal/users"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrRefreshTokenNotFound = errors.New("refresh token not found")
type RefreshSession struct {
ID string
TokenHash string
User users.User
ExpiresAt time.Time
RevokedAt *time.Time
}
type SessionStore struct {
db *pgxpool.Pool
}
func NewSessionStore(db *pgxpool.Pool) SessionStore {
return SessionStore{db: db}
}
func GenerateRefreshToken() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(bytes), nil
}
func HashRefreshToken(token string) string {
hash := sha256.Sum256([]byte(token))
return hex.EncodeToString(hash[:])
}
func (s SessionStore) CreateRefreshSession(ctx context.Context, userID string, tokenHash string, expiresAt time.Time) error {
_, err := s.db.Exec(ctx, `
INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
VALUES ($1::uuid, $2, $3)
`, userID, tokenHash, expiresAt)
return err
}
func (s SessionStore) FindValidRefreshSession(ctx context.Context, tokenHash string) (RefreshSession, error) {
row := s.db.QueryRow(ctx, `
SELECT
refresh_tokens.id::text,
refresh_tokens.token_hash,
users.id::text,
users.email,
users.name,
users.password_hash,
users.role,
users.status,
users.created_at,
users.updated_at,
refresh_tokens.expires_at,
refresh_tokens.revoked_at
FROM refresh_tokens
INNER JOIN users ON users.id = refresh_tokens.user_id
WHERE refresh_tokens.token_hash = $1
AND refresh_tokens.revoked_at IS NULL
AND refresh_tokens.expires_at > now()
AND users.status = 'active'
`, tokenHash)
var session RefreshSession
err := row.Scan(
&session.ID,
&session.TokenHash,
&session.User.ID,
&session.User.Email,
&session.User.Name,
&session.User.PasswordHash,
&session.User.Role,
&session.User.Status,
&session.User.CreatedAt,
&session.User.UpdatedAt,
&session.ExpiresAt,
&session.RevokedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return RefreshSession{}, ErrRefreshTokenNotFound
}
if err != nil {
return RefreshSession{}, err
}
return session, nil
}
func (s SessionStore) RevokeRefreshSession(ctx context.Context, tokenHash string) error {
_, err := s.db.Exec(ctx, `
UPDATE refresh_tokens
SET revoked_at = now()
WHERE token_hash = $1
AND revoked_at IS NULL
`, tokenHash)
return err
}