package repository import ( "context" "database/sql" "errors" "tailly_back_v2/internal/domain" ) var ( ErrRecoveryRequestNotFound = errors.New("recovery request not found") ErrRecoveryMethodNotFound = errors.New("recovery method not found") ) type RecoveryRepository interface { SaveRequest(ctx context.Context, req *domain.RecoveryRequest) error GetRequestByToken(ctx context.Context, token string) (*domain.RecoveryRequest, error) UpdateRequest(ctx context.Context, req *domain.RecoveryRequest) error GetMethods(ctx context.Context, userID int) ([]*domain.RecoveryMethod, error) SaveMethod(ctx context.Context, method *domain.RecoveryMethod) error } type recoveryRepository struct { db *sql.DB } func NewRecoveryRepository(db *sql.DB) RecoveryRepository { return &recoveryRepository{db: db} } func (r *recoveryRepository) SaveRequest(ctx context.Context, req *domain.RecoveryRequest) error { query := ` INSERT INTO recovery_requests ( user_id, token, new_device_id, status, created_at, expires_at ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id ` var deviceID interface{} if req.NewDevice != nil { deviceID = req.NewDevice.ID } else { deviceID = nil } err := r.db.QueryRowContext(ctx, query, req.UserID, req.Token, deviceID, req.Status, req.CreatedAt, req.ExpiresAt, ).Scan(&req.ID) return err } func (r *recoveryRepository) GetRequestByToken(ctx context.Context, token string) (*domain.RecoveryRequest, error) { query := ` SELECT r.id, r.user_id, r.token, r.status, r.created_at, r.expires_at, d.id, d.user_id, d.name, d.ip_address, d.user_agent, d.created_at FROM recovery_requests r LEFT JOIN devices d ON r.new_device_id = d.id WHERE r.token = $1 ` req := &domain.RecoveryRequest{} var device domain.Device var deviceID sql.NullInt64 err := r.db.QueryRowContext(ctx, query, token).Scan( &req.ID, &req.UserID, &req.Token, &req.Status, &req.CreatedAt, &req.ExpiresAt, &deviceID, &device.UserID, &device.Name, &device.IPAddress, &device.UserAgent, &device.CreatedAt, ) if err == sql.ErrNoRows { return nil, ErrRecoveryRequestNotFound } if err != nil { return nil, err } if deviceID.Valid { device.ID = int(deviceID.Int64) req.NewDevice = &device } return req, nil } func (r *recoveryRepository) UpdateRequest(ctx context.Context, req *domain.RecoveryRequest) error { query := ` UPDATE recovery_requests SET status = $1, new_device_id = $2 WHERE id = $3 ` var deviceID interface{} if req.NewDevice != nil { deviceID = req.NewDevice.ID } else { deviceID = nil } _, err := r.db.ExecContext(ctx, query, req.Status, deviceID, req.ID, ) return err } func (r *recoveryRepository) GetMethods(ctx context.Context, userID int) ([]*domain.RecoveryMethod, error) { query := ` SELECT id, user_id, method_type, value, is_primary, verified_at, created_at FROM recovery_methods WHERE user_id = $1 ORDER BY is_primary DESC, created_at ASC ` rows, err := r.db.QueryContext(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() var methods []*domain.RecoveryMethod for rows.Next() { var method domain.RecoveryMethod var verifiedAt sql.NullTime err := rows.Scan( &method.ID, &method.UserID, &method.MethodType, &method.Value, &method.IsPrimary, &verifiedAt, &method.CreatedAt, ) if err != nil { return nil, err } if verifiedAt.Valid { method.VerifiedAt = verifiedAt.Time } methods = append(methods, &method) } if len(methods) == 0 { return nil, ErrRecoveryMethodNotFound } return methods, nil } func (r *recoveryRepository) SaveMethod(ctx context.Context, method *domain.RecoveryMethod) error { query := ` INSERT INTO recovery_methods ( user_id, method_type, value, is_primary, verified_at, created_at ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id ` var verifiedAt interface{} if !method.VerifiedAt.IsZero() { verifiedAt = method.VerifiedAt } else { verifiedAt = nil } err := r.db.QueryRowContext(ctx, query, method.UserID, method.MethodType, method.Value, method.IsPrimary, verifiedAt, method.CreatedAt, ).Scan(&method.ID) return err }