Export all Firebase Authentication users to CSV with pagination support
Size
11.4 KB
Version
1.0.1
Created
Nov 6, 2025
Updated
about 1 month ago
1// ==UserScript==
2// @name Firebase Users CSV Exporter
3// @description Export all Firebase Authentication users to CSV with pagination support
4// @version 1.0.1
5// @match https://*.console.firebase.google.com/*
6// @icon https://www.gstatic.com/mobilesdk/240501_mobilesdk/firebase_16dp.png
7// @grant GM.xmlhttpRequest
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('Firebase Users CSV Exporter loaded');
13
14 // Debounce function to avoid multiple rapid calls
15 function debounce(func, wait) {
16 let timeout;
17 return function executedFunction(...args) {
18 const later = () => {
19 clearTimeout(timeout);
20 func(...args);
21 };
22 clearTimeout(timeout);
23 timeout = setTimeout(later, wait);
24 };
25 }
26
27 // Function to create and add the export button
28 function addExportButton() {
29 // Check if we're on the authentication users page
30 if (!window.location.href.includes('/authentication/users')) {
31 console.log('Not on authentication users page');
32 return;
33 }
34
35 // Check if button already exists
36 if (document.getElementById('firebase-csv-export-btn')) {
37 console.log('Export button already exists');
38 return;
39 }
40
41 // Find the "Add User" button
42 const addUserButton = document.querySelector('button[data-test-id="add-user-button"]');
43 if (!addUserButton) {
44 console.log('Add User button not found, will retry...');
45 return;
46 }
47
48 console.log('Add User button found, creating export button');
49
50 // Create the export button
51 const exportButton = document.createElement('button');
52 exportButton.id = 'firebase-csv-export-btn';
53 exportButton.className = 'mdc-button mat-mdc-button-base mdc-button--raised mat-mdc-raised-button mat-primary';
54 exportButton.style.marginLeft = '12px';
55 exportButton.innerHTML = `
56 <span class="mat-mdc-button-persistent-ripple mdc-button__ripple"></span>
57 <span class="mdc-button__label">Export CSV</span>
58 <span class="mat-focus-indicator"></span>
59 <span class="mat-mdc-button-touch-target"></span>
60 <span class="mat-ripple mat-mdc-button-ripple"></span>
61 `;
62
63 // Add click event listener
64 exportButton.addEventListener('click', handleExportClick);
65
66 // Insert the button next to the Add User button
67 addUserButton.parentElement.insertBefore(exportButton, addUserButton.nextSibling);
68 console.log('Export button added successfully');
69 }
70
71 // Function to handle export button click
72 async function handleExportClick() {
73 const button = document.getElementById('firebase-csv-export-btn');
74 const originalText = button.querySelector('.mdc-button__label').textContent;
75
76 try {
77 button.disabled = true;
78 button.querySelector('.mdc-button__label').textContent = 'Exporting...';
79
80 console.log('Starting CSV export...');
81
82 // Collect all user data from all pages
83 const allUsers = await collectAllUsers();
84
85 if (allUsers.length === 0) {
86 alert('No users found to export');
87 return;
88 }
89
90 console.log(`Collected ${allUsers.length} users, generating CSV...`);
91
92 // Generate CSV
93 const csv = generateCSV(allUsers);
94
95 // Download CSV
96 downloadCSV(csv, 'firebase-users.csv');
97
98 button.querySelector('.mdc-button__label').textContent = `Exported ${allUsers.length} users!`;
99 setTimeout(() => {
100 button.querySelector('.mdc-button__label').textContent = originalText;
101 }, 3000);
102
103 } catch (error) {
104 console.error('Export failed:', error);
105 alert('Export failed: ' + error.message);
106 button.querySelector('.mdc-button__label').textContent = originalText;
107 } finally {
108 button.disabled = false;
109 }
110 }
111
112 // Function to collect all users from all pages
113 async function collectAllUsers() {
114 const allUsers = [];
115 let currentPage = 0;
116
117 // Get current page users
118 const currentUsers = extractUsersFromCurrentPage();
119 allUsers.push(...currentUsers);
120 console.log(`Page ${currentPage + 1}: Collected ${currentUsers.length} users`);
121
122 // Get pagination info
123 const paginationInfo = getPaginationInfo();
124 console.log('Pagination info:', paginationInfo);
125
126 if (!paginationInfo) {
127 console.log('No pagination found, returning current page users');
128 return allUsers;
129 }
130
131 const totalPages = Math.ceil(paginationInfo.total / paginationInfo.pageSize);
132 console.log(`Total pages to process: ${totalPages}`);
133
134 // Navigate through all pages
135 for (let page = 1; page < totalPages; page++) {
136 const button = document.getElementById('firebase-csv-export-btn');
137 button.querySelector('.mdc-button__label').textContent = `Exporting... (${page + 1}/${totalPages})`;
138
139 // Click next page button
140 const nextPageClicked = await clickNextPage();
141 if (!nextPageClicked) {
142 console.log('Could not click next page, stopping');
143 break;
144 }
145
146 // Wait for page to load
147 await waitForPageLoad();
148
149 // Extract users from this page
150 const pageUsers = extractUsersFromCurrentPage();
151 allUsers.push(...pageUsers);
152 console.log(`Page ${page + 1}: Collected ${pageUsers.length} users (Total: ${allUsers.length})`);
153 }
154
155 return allUsers;
156 }
157
158 // Function to extract users from the current page
159 function extractUsersFromCurrentPage() {
160 const users = [];
161 const rows = document.querySelectorAll('table[role="grid"] tbody tr[role="row"]');
162
163 console.log(`Found ${rows.length} user rows on current page`);
164
165 rows.forEach((row, index) => {
166 try {
167 const cells = row.querySelectorAll('td[role="gridcell"]');
168 if (cells.length >= 4) {
169 const identifier = cells[0].textContent.trim();
170 const created = cells[2].textContent.trim();
171 const signedIn = cells[3].textContent.trim();
172
173 users.push({
174 identifier,
175 created,
176 signedIn
177 });
178 }
179 } catch (error) {
180 console.error(`Error extracting user from row ${index}:`, error);
181 }
182 });
183
184 return users;
185 }
186
187 // Function to get pagination information
188 function getPaginationInfo() {
189 const paginationLabel = document.querySelector('.mat-mdc-paginator-range-label');
190 if (!paginationLabel) {
191 return null;
192 }
193
194 const text = paginationLabel.textContent.trim();
195 // Format: "1 – 250 of 2784"
196 const match = text.match(/(\d+)\s*–\s*(\d+)\s*of\s*(\d+)/);
197
198 if (match) {
199 return {
200 start: parseInt(match[1]),
201 end: parseInt(match[2]),
202 total: parseInt(match[3]),
203 pageSize: parseInt(match[2]) - parseInt(match[1]) + 1
204 };
205 }
206
207 return null;
208 }
209
210 // Function to click the next page button
211 async function clickNextPage() {
212 const nextButton = document.querySelector('button.mat-mdc-paginator-navigation-next:not(.mat-mdc-button-disabled)');
213 if (!nextButton) {
214 console.log('Next button not found or disabled');
215 return false;
216 }
217
218 nextButton.click();
219 return true;
220 }
221
222 // Function to wait for page to load
223 function waitForPageLoad() {
224 return new Promise((resolve) => {
225 // Wait for the table to update
226 let attempts = 0;
227 const maxAttempts = 50; // 5 seconds max
228
229 const checkInterval = setInterval(() => {
230 attempts++;
231
232 // Check if loading indicator is gone and table has content
233 const rows = document.querySelectorAll('table[role="grid"] tbody tr[role="row"]');
234 const loadingIndicator = document.querySelector('.mat-mdc-progress-spinner, .loading-spinner');
235
236 if (rows.length > 0 && !loadingIndicator) {
237 clearInterval(checkInterval);
238 // Add a small delay to ensure data is fully rendered
239 setTimeout(resolve, 500);
240 } else if (attempts >= maxAttempts) {
241 clearInterval(checkInterval);
242 console.log('Timeout waiting for page load');
243 resolve();
244 }
245 }, 100);
246 });
247 }
248
249 // Function to generate CSV from user data
250 function generateCSV(users) {
251 const headers = ['Identifier', 'Created', 'Signed In'];
252 const rows = [headers];
253
254 users.forEach(user => {
255 rows.push([
256 escapeCSVField(user.identifier),
257 escapeCSVField(user.created),
258 escapeCSVField(user.signedIn)
259 ]);
260 });
261
262 return rows.map(row => row.join(',')).join('\n');
263 }
264
265 // Function to escape CSV fields
266 function escapeCSVField(field) {
267 if (field.includes(',') || field.includes('"') || field.includes('\n')) {
268 return `"${field.replace(/"/g, '""')}"`;
269 }
270 return field;
271 }
272
273 // Function to download CSV file
274 function downloadCSV(csvContent, filename) {
275 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
276 const link = document.createElement('a');
277 const url = URL.createObjectURL(blob);
278
279 link.setAttribute('href', url);
280 link.setAttribute('download', filename);
281 link.style.visibility = 'hidden';
282
283 document.body.appendChild(link);
284 link.click();
285 document.body.removeChild(link);
286
287 console.log('CSV download initiated');
288 }
289
290 // Initialize the extension
291 function init() {
292 console.log('Initializing Firebase CSV Exporter...');
293
294 // Try to add button immediately
295 addExportButton();
296
297 // Watch for DOM changes to add button when page loads
298 const debouncedAddButton = debounce(addExportButton, 500);
299 const observer = new MutationObserver(debouncedAddButton);
300
301 observer.observe(document.body, {
302 childList: true,
303 subtree: true
304 });
305
306 // Also check periodically in case mutation observer misses it
307 const checkInterval = setInterval(() => {
308 if (document.getElementById('firebase-csv-export-btn')) {
309 clearInterval(checkInterval);
310 } else {
311 addExportButton();
312 }
313 }, 2000);
314 }
315
316 // Start when DOM is ready
317 if (document.readyState === 'loading') {
318 document.addEventListener('DOMContentLoaded', init);
319 } else {
320 init();
321 }
322
323})();