Export all Firebase Authentication users to CSV with identifier, created date, and signed in date
Size
11.0 KB
Version
1.0.1
Created
Nov 29, 2025
Updated
10 days ago
1// ==UserScript==
2// @name Firebase Users CSV Exporter
3// @description Export all Firebase Authentication users to CSV with identifier, created date, and signed in date
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: Extension loaded');
13
14 // Debounce function to prevent 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 convert data to CSV format
28 function convertToCSV(data) {
29 if (data.length === 0) return '';
30
31 const headers = ['Identifier', 'Created', 'Signed In'];
32 const csvRows = [headers.join(',')];
33
34 for (const row of data) {
35 const values = [
36 `"${row.identifier || ''}"`,
37 `"${row.created || ''}"`,
38 `"${row.signedIn || ''}"`
39 ];
40 csvRows.push(values.join(','));
41 }
42
43 return csvRows.join('\n');
44 }
45
46 // Function to download CSV file
47 function downloadCSV(csvContent, filename) {
48 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
49 const link = document.createElement('a');
50 const url = URL.createObjectURL(blob);
51
52 link.setAttribute('href', url);
53 link.setAttribute('download', filename);
54 link.style.visibility = 'hidden';
55 document.body.appendChild(link);
56 link.click();
57 document.body.removeChild(link);
58
59 console.log('Firebase Users CSV Exporter: CSV file downloaded');
60 }
61
62 // Function to extract user data from the current page
63 function extractUserDataFromPage() {
64 const users = [];
65 const rows = document.querySelectorAll('table[role="grid"] tbody tr[role="row"]');
66
67 console.log(`Firebase Users CSV Exporter: Found ${rows.length} user rows on current page`);
68
69 rows.forEach(row => {
70 const identifierCell = row.querySelector('td.cdk-column-identifier');
71 const createdCell = row.querySelector('td.cdk-column-created-at');
72 const signedInCell = row.querySelector('td.cdk-column-last-login');
73
74 if (identifierCell) {
75 const identifier = identifierCell.textContent.trim();
76 const created = createdCell ? createdCell.textContent.trim() : '';
77 const signedIn = signedInCell ? signedInCell.textContent.trim() : '';
78
79 users.push({
80 identifier: identifier,
81 created: created,
82 signedIn: signedIn
83 });
84 }
85 });
86
87 return users;
88 }
89
90 // Function to get total number of pages
91 function getTotalPages() {
92 const paginatorLabel = document.querySelector('.mat-mdc-paginator-range-label');
93 if (paginatorLabel) {
94 const text = paginatorLabel.textContent.trim();
95 // Format: "1 – 50 of 3101"
96 const match = text.match(/of\s+(\d+)/);
97 if (match) {
98 const totalUsers = parseInt(match[1]);
99 const rowsPerPageSelect = document.querySelector('.mat-mdc-paginator-page-size-select .mat-mdc-select-value-text');
100 const rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.textContent.trim()) : 50;
101 return Math.ceil(totalUsers / rowsPerPage);
102 }
103 }
104 return 1;
105 }
106
107 // Function to click next page button
108 function clickNextPage() {
109 const nextButton = document.querySelector('button.mat-mdc-paginator-navigation-next:not(.mat-mdc-button-disabled)');
110 if (nextButton) {
111 nextButton.click();
112 return true;
113 }
114 return false;
115 }
116
117 // Function to wait for page to load
118 function waitForPageLoad(timeout = 5000) {
119 return new Promise((resolve) => {
120 let elapsed = 0;
121 const interval = 100;
122
123 const checkLoading = setInterval(() => {
124 const rows = document.querySelectorAll('table[role="grid"] tbody tr[role="row"]');
125 if (rows.length > 0) {
126 clearInterval(checkLoading);
127 // Wait a bit more to ensure data is fully loaded
128 setTimeout(resolve, 500);
129 } else if (elapsed >= timeout) {
130 clearInterval(checkLoading);
131 resolve();
132 }
133 elapsed += interval;
134 }, interval);
135 });
136 }
137
138 // Main export function
139 async function exportAllUsers(button) {
140 console.log('Firebase Users CSV Exporter: Starting export process');
141
142 // Disable button and show loading state
143 const originalText = button.textContent;
144 button.disabled = true;
145 button.textContent = 'Exporting...';
146 button.style.opacity = '0.6';
147
148 try {
149 const allUsers = [];
150 const totalPages = getTotalPages();
151 console.log(`Firebase Users CSV Exporter: Total pages to process: ${totalPages}`);
152
153 // Extract data from current page
154 const currentPageUsers = extractUserDataFromPage();
155 allUsers.push(...currentPageUsers);
156 console.log(`Firebase Users CSV Exporter: Collected ${currentPageUsers.length} users from page 1`);
157
158 // Navigate through all pages
159 for (let page = 2; page <= totalPages; page++) {
160 const hasNextPage = clickNextPage();
161 if (!hasNextPage) {
162 console.log('Firebase Users CSV Exporter: No more pages available');
163 break;
164 }
165
166 // Wait for next page to load
167 await waitForPageLoad();
168
169 // Extract data from this page
170 const pageUsers = extractUserDataFromPage();
171 allUsers.push(...pageUsers);
172 console.log(`Firebase Users CSV Exporter: Collected ${pageUsers.length} users from page ${page} (Total: ${allUsers.length})`);
173
174 // Update button text with progress
175 button.textContent = `Exporting... (${allUsers.length} users)`;
176 }
177
178 console.log(`Firebase Users CSV Exporter: Total users collected: ${allUsers.length}`);
179
180 // Convert to CSV and download
181 const csvContent = convertToCSV(allUsers);
182 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
183 const filename = `firebase-users-${timestamp}.csv`;
184 downloadCSV(csvContent, filename);
185
186 // Show success message
187 button.textContent = `✓ Exported ${allUsers.length} users`;
188 button.style.backgroundColor = '#4caf50';
189
190 // Reset button after 3 seconds
191 setTimeout(() => {
192 button.textContent = originalText;
193 button.disabled = false;
194 button.style.opacity = '1';
195 button.style.backgroundColor = '';
196 }, 3000);
197
198 } catch (error) {
199 console.error('Firebase Users CSV Exporter: Error during export:', error);
200 button.textContent = '✗ Export failed';
201 button.style.backgroundColor = '#f44336';
202
203 // Reset button after 3 seconds
204 setTimeout(() => {
205 button.textContent = originalText;
206 button.disabled = false;
207 button.style.opacity = '1';
208 button.style.backgroundColor = '';
209 }, 3000);
210 }
211 }
212
213 // Function to create and add the export button
214 function addExportButton() {
215 // Check if we're on the authentication users page
216 if (!window.location.href.includes('/authentication/users')) {
217 console.log('Firebase Users CSV Exporter: Not on authentication users page');
218 return;
219 }
220
221 // Check if button already exists
222 if (document.getElementById('firebase-export-csv-button')) {
223 return;
224 }
225
226 // Find the "Add User" button
227 const addUserButton = document.querySelector('button[data-test-id="add-user-button"]');
228 if (!addUserButton) {
229 console.log('Firebase Users CSV Exporter: Add User button not found yet');
230 return;
231 }
232
233 console.log('Firebase Users CSV Exporter: Adding export button');
234
235 // Create export button with matching style
236 const exportButton = document.createElement('button');
237 exportButton.id = 'firebase-export-csv-button';
238 exportButton.className = addUserButton.className;
239 exportButton.innerHTML = `
240 <span class="mat-mdc-button-persistent-ripple mdc-button__ripple"></span>
241 <span class="mdc-button__label">Export to CSV</span>
242 <span class="mat-focus-indicator"></span>
243 <span class="mat-mdc-button-touch-target"></span>
244 `;
245 exportButton.style.marginLeft = '12px';
246
247 // Add click event listener
248 exportButton.addEventListener('click', () => exportAllUsers(exportButton));
249
250 // Insert button next to Add User button
251 addUserButton.parentNode.insertBefore(exportButton, addUserButton.nextSibling);
252
253 console.log('Firebase Users CSV Exporter: Export button added successfully');
254 }
255
256 // Initialize the extension
257 function init() {
258 console.log('Firebase Users CSV Exporter: Initializing');
259
260 // Try to add button immediately
261 addExportButton();
262
263 // Watch for DOM changes to add button when page loads
264 const debouncedAddButton = debounce(addExportButton, 500);
265 const observer = new MutationObserver(debouncedAddButton);
266
267 observer.observe(document.body, {
268 childList: true,
269 subtree: true
270 });
271
272 // Also check when URL changes (for SPA navigation)
273 let lastUrl = location.href;
274 new MutationObserver(() => {
275 const url = location.href;
276 if (url !== lastUrl) {
277 lastUrl = url;
278 console.log('Firebase Users CSV Exporter: URL changed, checking for button');
279 setTimeout(addExportButton, 1000);
280 }
281 }).observe(document, { subtree: true, childList: true });
282 }
283
284 // Start when DOM is ready
285 if (document.readyState === 'loading') {
286 document.addEventListener('DOMContentLoaded', init);
287 } else {
288 init();
289 }
290})();