Files
backend/internal/repository/session_repo.go
lafay eb931bf1c6
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 46s
feat(auth): implement session-based token management with revocation support
Add Session model, SessionService, and SessionRepository to track user login
sessions and enable token revocation on auth-critical events.

- Introduce explicit TokenType (access/refresh) in JWT claims to prevent
  refresh token misuse via access token endpoints
- Add SessionID field to JWT claims, enabling stateless JWT validation
  against revoked sessions
- Replace legacy Auth middleware with RequireAuth/OptionalAuth pipeline
  that validates token type, account status, and session validity
- Implement session revocation on password change, reset, user ban/inactive,
  and explicit logout
- Add Principal cache with active invalidation for banned/role-changed users
- Fix IDOR vulnerability: GetMessagesByCursor now validates currentUserID
  is conversation participant via GetParticipantStrict
- Add group member visibility checks: announcements, group info, member list
  now require group membership
- Simplify Casbin policy: remove g grouping, use r.sub == p.sub matcher
  with globMatch; user_roles table is single source of truth for roles
- Add migration logic to clean legacy casbin g rules and migrate old p rules
  from path-style to admin/<domain> resource naming
2026-07-05 18:28:08 +08:00

90 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package repository
import (
"context"
"time"
"with_you/internal/model"
"gorm.io/gorm"
)
// SessionRepository 会话仓储接口
//
// 用于支持令牌撤销:登出/封禁按会话或用户撤销refresh token 轮换按 hash 反查。
type SessionRepository interface {
Create(ctx context.Context, session *model.Session) error
GetByID(ctx context.Context, id string) (*model.Session, error)
GetByRefreshTokenHash(ctx context.Context, hash string) (*model.Session, error)
Update(ctx context.Context, session *model.Session) error
Revoke(ctx context.Context, id string) error
RevokeAllByUser(ctx context.Context, userID string) error
RevokeByRefreshTokenHash(ctx context.Context, hash string) error
DeleteExpired(ctx context.Context, before time.Time) error
}
type sessionRepository struct {
db *gorm.DB
}
// NewSessionRepository 创建会话仓储。
func NewSessionRepository(db *gorm.DB) SessionRepository {
return &sessionRepository{db: db}
}
func (r *sessionRepository) Create(ctx context.Context, session *model.Session) error {
return r.db.WithContext(ctx).Create(session).Error
}
func (r *sessionRepository) GetByID(ctx context.Context, id string) (*model.Session, error) {
var s model.Session
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&s).Error; err != nil {
return nil, err
}
return &s, nil
}
func (r *sessionRepository) GetByRefreshTokenHash(ctx context.Context, hash string) (*model.Session, error) {
var s model.Session
if err := r.db.WithContext(ctx).Where("refresh_token_hash = ?", hash).First(&s).Error; err != nil {
return nil, err
}
return &s, nil
}
func (r *sessionRepository) Update(ctx context.Context, session *model.Session) error {
// 仅更新业务关心的列,避免 Save 全字段写回在并发 Rotate 下覆盖 RevokedAt 等字段造成 lost update。
return r.db.WithContext(ctx).Model(&model.Session{}).
Where("id = ?", session.ID).
Updates(map[string]interface{}{
"last_used_at": session.LastUsedAt,
"refresh_token_hash": session.RefreshTokenHash,
}).Error
}
func (r *sessionRepository) Revoke(ctx context.Context, id string) error {
now := time.Now()
return r.db.WithContext(ctx).Model(&model.Session{}).
Where("id = ? AND revoked_at IS NULL", id).
Update("revoked_at", now).Error
}
func (r *sessionRepository) RevokeAllByUser(ctx context.Context, userID string) error {
now := time.Now()
return r.db.WithContext(ctx).Model(&model.Session{}).
Where("user_id = ? AND revoked_at IS NULL", userID).
Update("revoked_at", now).Error
}
func (r *sessionRepository) RevokeByRefreshTokenHash(ctx context.Context, hash string) error {
now := time.Now()
return r.db.WithContext(ctx).Model(&model.Session{}).
Where("refresh_token_hash = ? AND revoked_at IS NULL", hash).
Update("revoked_at", now).Error
}
func (r *sessionRepository) DeleteExpired(ctx context.Context, before time.Time) error {
return r.db.WithContext(ctx).
Where("expires_at < ? OR revoked_at IS NOT NULL", before).
Delete(&model.Session{}).Error
}