File size: 12,704 Bytes
04aa1ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0b765c
04aa1ba
f0b765c
 
 
 
 
 
04aa1ba
f0b765c
04aa1ba
 
f0b765c
04aa1ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#!/usr/bin/env python3
"""
Rollback script for user authentication migration.

This script safely removes user_id fields and indexes added by the
user authentication migration, restoring the database to its previous state.

Usage:
    python rollback_user_authentication.py [--confirm] [--backup-first]

Options:
    --confirm       Skip confirmation prompt (for automated scripts)
    --backup-first  Create backup before rollback
"""

import argparse
import asyncio
from datetime import datetime
import json
import logging
import os
import os
import sys
from typing import Any, Dict

from dotenv import load_dotenv

from analytics.create_indexes import drop_user_id_indexes
from analytics.database import get_database, test_connection

        timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
        backup_file = os.path.join(self.backup_dir, f"pre_rollback_backup_{timestamp}.json")
        
        logger.info(f"Creating pre-rollback backup: {backup_file}")
        
        try:
            backup_data = {
                "timestamp": timestamp,
                "backup_type": "pre_rollback",
                "collections": {}
            }
            
            # Backup user_id data from each collection
            for collection_name in ["sessions", "messages", "search_analytics"]:
                try:
                    collection = self.db[collection_name]
                    
                    # Only backup documents with user_id field
                    documents = await collection.find({"user_id": {"$exists": True}}).to_list(length=None)
                    
                    # Convert to serializable format
                    serializable_docs = []
                    for doc in documents:
                        serializable_doc = {}
                        for key, value in doc.items():
                            if isinstance(value, datetime):
                                serializable_doc[key] = value.isoformat()
                            else:
                                serializable_doc[key] = str(value) if hasattr(value, '__str__') else value
                        serializable_docs.append(serializable_doc)
                    
                    backup_data["collections"][collection_name] = {
                        "count": len(documents),
                        "documents": serializable_docs
                    }
                    
                    logger.info(f"Backed up {len(documents)} documents with user_id from {collection_name}")
                    
                except Exception as e:
                    logger.warning(f"Could not backup {collection_name}: {e}")
                    backup_data["collections"][collection_name] = {"error": str(e)}
            
            # Write backup file
            with open(backup_file, 'w') as f:
                json.dump(backup_data, f, indent=2)
            
            logger.info(f"Pre-rollback backup completed: {backup_file}")
            return backup_file
            
        except Exception as e:
            logger.error(f"Backup failed: {e}")
            raise
    
    async def execute_rollback(self) -> bool:
        """Execute the complete rollback process"""
        try:
            logger.info("Starting user authentication rollback...")
            
            # Create backup if requested
            backup_file = None
            if self.backup_first:
                backup_file = await self.create_rollback_backup()
            
            # Record rollback start in migration history
            await self._record_rollback_event("rollback_started", {
                "backup_file": backup_file
            })
            
            # Step 1: Drop user_id indexes
            logger.info("Step 1: Dropping user_id indexes...")
            index_success = await drop_user_id_indexes()
            
            if not index_success:
                logger.warning("Some indexes could not be dropped (they may not exist)")
            
            # Step 2: Remove user_id fields from documents
            logger.info("Step 2: Removing user_id fields from documents...")
            field_success = await self._remove_user_id_fields()
            
            if not field_success:
                await self._record_rollback_event("rollback_failed", {"step": "remove_fields"})
                return False
            
            # Step 3: Verify rollback
            logger.info("Step 3: Verifying rollback...")
            verification_success = await self._verify_rollback()
            
            if not verification_success:
                await self._record_rollback_event("rollback_failed", {"step": "verification"})
                return False
            
            # Record successful completion
            await self._record_rollback_event("rollback_completed", {
                "backup_file": backup_file,
                "verification_passed": True
            })
            
            logger.info("Rollback completed successfully!")
            return True
            
        except Exception as e:
            logger.error(f"Rollback failed: {e}")
            await self._record_rollback_event("rollback_failed", {"error": str(e)})
            return False
    
    async def _remove_user_id_fields(self) -> bool:
        """Remove user_id fields from all documents"""
        try:
            collections_to_update = ["sessions", "messages", "search_analytics"]
            
            for collection_name in collections_to_update:
                logger.info(f"Removing user_id field from {collection_name}...")
                
                collection = self.db[collection_name]
                
                # Remove user_id field from all documents
                result = await collection.update_many(
                    {"user_id": {"$exists": True}},
                    {"$unset": {"user_id": ""}}
                )
                
                logger.info(f"Removed user_id field from {result.modified_count} documents in {collection_name}")
            
            return True
            
        except Exception as e:
            logger.error(f"Error removing user_id fields: {e}")
            return False
    
    async def _verify_rollback(self) -> bool:
        """Verify that rollback was successful"""
        try:
            logger.info("Verifying rollback completion...")
            
            collections_to_check = ["sessions", "messages", "search_analytics"]
            
            for collection_name in collections_to_check:
                collection = self.db[collection_name]
                
                # Check that no documents have user_id field
                docs_with_user_id = await collection.count_documents({"user_id": {"$exists": True}})
                
                if docs_with_user_id > 0:
                    logger.error(f"Rollback verification failed: {docs_with_user_id} documents in {collection_name} still have user_id field")
                    return False
                else:
                    logger.info(f"βœ“ No user_id fields found in {collection_name}")
            
            # Check that user_id indexes are gone
            expected_user_indexes = [
                "user_id_sparse",
                "user_id_start_time_compound",
                "user_id_timestamp_compound"
            ]
            
            for collection_name in collections_to_check:
                collection = self.db[collection_name]
                indexes = await collection.list_indexes().to_list(length=None)
                index_names = [idx["name"] for idx in indexes]
                
                remaining_user_indexes = [name for name in expected_user_indexes if name in index_names]
                
                if remaining_user_indexes:
                    logger.warning(f"Some user_id indexes still exist in {collection_name}: {remaining_user_indexes}")
                    # This is a warning, not a failure
                else:
                    logger.info(f"βœ“ No user_id indexes found in {collection_name}")
            
            logger.info("βœ… Rollback verification passed!")
            return True
            
        except Exception as e:
            logger.error(f"Rollback verification failed: {e}")
            return False
    
    async def _record_rollback_event(self, status: str, details: Dict[str, Any]):
        """Record rollback event in migration history"""
        try:
            # Check if migration_history collection exists
            collections = await self.db.list_collection_names()
            if "migration_history" not in collections:
                logger.warning("Migration history collection does not exist, cannot record rollback event")
                return
            
            migration_coll = self.db["migration_history"]
            
            event = {
                "migration_name": "user_authentication",
                "status": status,
                "timestamp": datetime.utcnow(),
                "details": details,
                "operation_type": "rollback"
            }
            
            await migration_coll.insert_one(event)
            
        except Exception as e:
            logger.warning(f"Could not record rollback event: {e}")

def setup_logging():
    """Setup logging configuration"""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

async def main():
    """Main CLI function"""
    parser = argparse.ArgumentParser(description="Rollback user authentication migration")
    parser.add_argument("--confirm", action="store_true", help="Skip confirmation prompt")
    parser.add_argument("--backup-first", action="store_true", help="Create backup before rollback")
    
    args = parser.parse_args()
    
    setup_logging()
    
    print(f"πŸ”„ User Authentication Migration Rollback")
    print(f"Backup first: {args.backup_first}")
    print("-" * 50)
    
    try:
        # Test database connection
        if not await test_connection():
            print("❌ Database connection failed. Check your MONGODB_URL.")
            sys.exit(1)
        
        # Initialize rollback manager
        manager = RollbackManager(backup_first=args.backup_first)
        await manager.initialize()
        
        # Check rollback feasibility
        print("Checking rollback feasibility...")
        feasibility = await manager.check_rollback_feasibility()
        
        if not feasibility.get("can_rollback", False):
            print(f"❌ Rollback not feasible: {feasibility.get('error', 'Unknown error')}")
            sys.exit(1)
        
        # Show warnings
        if feasibility.get("warnings"):
            print("\n⚠️  Rollback Warnings:")
            for warning in feasibility["warnings"]:
                print(f"  - {warning}")
        
        # Show data loss risk
        if feasibility.get("data_loss_risk"):
            print("\n🚨 DATA LOSS WARNING:")
            print("This rollback will permanently delete user_id associations.")
            print("User-specific analytics data will be lost.")
        
        # Show collection status
        print(f"\nπŸ“Š Collections Status:")
        for collection, status in feasibility.get("collections_status", {}).items():
            if isinstance(status, dict):
                print(f"  {collection}: {status['docs_with_user_id']}/{status['total_docs']} docs have user_id")
            else:
                print(f"  {collection}: {status}")
        
        # Confirmation prompt
        if not args.confirm:
            print(f"\n❓ Are you sure you want to proceed with rollback?")
            if feasibility.get("data_loss_risk"):
                print("   This will permanently delete user authentication data!")
            
            response = input("Type 'yes' to confirm: ").strip().lower()
            if response != 'yes':
                print("Rollback cancelled.")
                sys.exit(0)
        
        # Execute rollback
        print("\nExecuting rollback...")
        success = await manager.execute_rollback()
        
        if success:
            print("βœ… Rollback completed successfully!")
            print("\nThe database has been restored to its pre-migration state.")
            print("All user_id fields and indexes have been removed.")
        else:
            print("❌ Rollback failed! Check logs for details.")
            sys.exit(1)
            
    except Exception as e:
        print(f"❌ Rollback failed: {e}")
        sys.exit(1)

if __name__ == "__main__":
    asyncio.run(main())