package repository import ( "context" "time" "gorm.io/gorm" "github.com/gochat/gochat/internal/model" ) // AccessTokenRepo provides data access for AccessToken with hashed token storage // and prefix-based lookup (Chatwoot pattern) for security. // Reference: Chatwoot app/models/access_token.rb + AccessTokenable concern type AccessTokenRepo struct { db *gorm.DB } // NewAccessTokenRepo creates a new AccessToken repository. func NewAccessTokenRepo(db *gorm.DB) *AccessTokenRepo { return &AccessTokenRepo{db: db} } // Create inserts a new AccessToken record. func (r *AccessTokenRepo) Create(ctx context.Context, token *model.AccessToken) error { return r.db.WithContext(ctx).Create(token).Error } // GetByID retrieves an AccessToken by primary key. func (r *AccessTokenRepo) GetByID(ctx context.Context, id uint) (*model.AccessToken, error) { var token model.AccessToken if err := r.db.WithContext(ctx).First(&token, id).Error; err != nil { return nil, err } return &token, nil } // FindByTokenPrefix retrieves an AccessToken by its token prefix and owner type. // This is the primary auth lookup path: the caller extracts the first ~8 chars of // the plaintext token, looks up the matching record via prefix, then compares the // full SHA-256 hash stored in Token. Using a prefix index avoids scanning the full // hashed-token unique index. func (r *AccessTokenRepo) FindByTokenPrefix(ctx context.Context, prefix, ownerType string) (*model.AccessToken, error) { var token model.AccessToken if err := r.db.WithContext(ctx). Where("token_prefix LIKE ?", prefix). Where("owner_type = ?", ownerType). First(&token).Error; err != nil { return nil, err } return &token, nil } // FindByOwner retrieves all AccessTokens for a given owner (non-deleted). func (r *AccessTokenRepo) FindByOwner(ctx context.Context, ownerType string, ownerID uint) ([]model.AccessToken, error) { var tokens []model.AccessToken if err := r.db.WithContext(ctx). Where("owner_type = ? AND owner_id = ?", ownerType, ownerID). Find(&tokens).Error; err != nil { return nil, err } return tokens, nil } // FindActiveByOwner retrieves all non-expired, non-deleted AccessTokens for a given owner. // Tokens with ExpiresAt before now are excluded. func (r *AccessTokenRepo) FindActiveByOwner(ctx context.Context, ownerType string, ownerID uint) ([]model.AccessToken, error) { var tokens []model.AccessToken now := time.Now() if err := r.db.WithContext(ctx). Where("owner_type = ? AND owner_id = ?", ownerType, ownerID). Where("expires_at IS NULL OR expires_at > ?", now). Find(&tokens).Error; err != nil { return nil, err } return tokens, nil } // Update saves changes to an existing AccessToken. func (r *AccessTokenRepo) Update(ctx context.Context, token *model.AccessToken) error { return r.db.WithContext(ctx).Save(token).Error } // UpdateLastUsedAt records the current time as LastUsedAt for the given token ID. func (r *AccessTokenRepo) UpdateLastUsedAt(ctx context.Context, id uint) error { now := time.Now() return r.db.WithContext(ctx). Model(&model.AccessToken{}). Where("id = ?", id). Update("last_used_at", now).Error } // Delete soft-deletes an AccessToken by ID (sets DeletedAt via gorm.DeletedAt). func (r *AccessTokenRepo) Delete(ctx context.Context, id uint) error { return r.db.WithContext(ctx).Delete(&model.AccessToken{}, id).Error }