Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Firestudio is a **free and open-source desktop application** for managing your c
- 👥 **Authentication**: View, create, and manage Firebase Auth users
- 🔍 **Querying**: Use simple filters or JavaScript for advanced queries
- 📥 **Import/Export**: Bulk operations with JSON files
- 🔧 **Local Emulator Support**: Connect to Firebase Local Emulator Suite
- 🎨 **Themes**: Dark/Light mode with customizable settings

Perfect for **Firebase developers**, **backend engineers**, **database administrators**, and anyone working with **Google Firebase**.
Expand Down Expand Up @@ -52,6 +53,7 @@ Perfect for **Firebase developers**, **backend engineers**, **database administr

- **Service Account**: Connect using Firebase service account JSON files for full admin access
- **Google Sign-In**: OAuth-based authentication using your Google account
- **Local Emulator**: Connect directly to Firebase Local Emulator Suite for local development

### 📊 Firestore Database Management

Expand Down Expand Up @@ -182,6 +184,12 @@ pnpm run dev

See detailed OAuth setup guide in the [Google Sign-In Setup](#google-sign-in-setup) section.

#### Method 3: Local Emulator

1. Start your Firebase Emulators (`firebase emulators:start`)
2. In Firestudio, click **"Add Project"** → **"Local Emulator"** tab
3. Select a detected emulator from the list and click **"Connect"**

---

## 🛠️ Tech Stack
Expand Down
165 changes: 158 additions & 7 deletions electron/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,82 @@
/**
* Auth Controller
* Handles Firebase Authentication user management via IPC (Admin SDK only)
* Handles Firebase Authentication user management via IPC (Admin SDK / Emulator REST)
* Note: Google OAuth auth operations are handled by googleController.js
*/

const { ipcMain } = require('electron');
const fetch = require('node-fetch');

let adminRef = null;
let authEmulatorHost = null;

function setAdminRef(admin) {
adminRef = admin;
}

function setAuthEmulatorHost(host) {
authEmulatorHost = host;
}

function getProjectId() {
if (adminRef?.apps?.length > 0) {
return adminRef.app().options.projectId;
}
return null;
}

async function getAuthEmulatorBase() {
if (authEmulatorHost) return `http://${authEmulatorHost}/identitytoolkit.googleapis.com/v1`;
return null;
}

function normalizeUserRecord(user) {
return {
uid: user.localId || user.uid,
email: user.email || null,
emailVerified: user.emailVerified || false,
displayName: user.displayName || null,
photoURL: user.photoUrl || null,
phoneNumber: user.phoneNumber || null,
disabled: user.disabled || false,
metadata: user.createdAt
? {
creationTime: new Date(Number(user.createdAt)).toISOString(),
lastSignInTime: user.lastLoginAt ? new Date(Number(user.lastLoginAt)).toISOString() : null,
}
: { creationTime: null, lastSignInTime: null },
providerData: (user.providerUserInfo || []).map((p) => ({
providerId: p.providerId,
uid: p.rawId || p.federatedId,
email: p.email || null,
displayName: p.displayName || null,
photoURL: p.photoUrl || null,
})),
};
}

function registerHandlers() {
// List users (Admin SDK)
// List users
ipcMain.handle('auth:listUsers', async (event, { maxResults = 1000 } = {}) => {
try {
const baseUrl = await getAuthEmulatorBase();
const projectId = getProjectId();
if (!projectId) throw new Error('Not connected to Firebase');

if (baseUrl) {
const response = await fetch(`${baseUrl}/projects/${projectId}/accounts:query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer owner' },
body: JSON.stringify({ returnUserInfo: true, maxResults }),
});
const data = await response.json();
if (data.error) {
return { success: false, error: data.error.message || 'List users failed' };
}
const users = (data.userInfo || []).map(normalizeUserRecord);
return { success: true, users };
}

if (!adminRef?.apps?.length) throw new Error('Not connected to Firebase');
const listUsersResult = await adminRef.auth().listUsers(maxResults);
const users = listUsersResult.users.map((user) => ({
Expand All @@ -42,11 +103,37 @@ function registerHandlers() {
}
});

// Create user (Admin SDK)
// Create user
ipcMain.handle(
'auth:createUser',
async (event, { email, password, displayName, phoneNumber, uid, photoURL, disabled, emailVerified }) => {
try {
const baseUrl = await getAuthEmulatorBase();
const projectId = getProjectId();
if (!projectId) throw new Error('Not connected to Firebase');

if (baseUrl) {
const body = { email, password };
if (displayName) body.displayName = displayName;
if (phoneNumber) body.phoneNumber = phoneNumber;
if (uid) body.localId = uid;
if (disabled !== undefined) body.disabled = disabled;
if (emailVerified !== undefined) body.emailVerified = emailVerified;
const response = await fetch(`${baseUrl}/projects/${projectId}/accounts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer owner' },
body: JSON.stringify(body),
});
const data = await response.json();
if (data.error) {
return { success: false, error: data.error.message || 'Create user failed' };
}
return {
success: true,
user: { uid: data.localId, email: data.email, displayName: data.displayName || null },
};
}

if (!adminRef?.apps?.length) throw new Error('Not connected to Firebase');
const userData = { email, password };
if (displayName) userData.displayName = displayName;
Expand All @@ -66,11 +153,38 @@ function registerHandlers() {
},
);

// Update user (Admin SDK)
// Update user
ipcMain.handle(
'auth:updateUser',
async (event, { uid, email, password, displayName, phoneNumber, disabled, photoURL, emailVerified }) => {
try {
const baseUrl = await getAuthEmulatorBase();
const projectId = getProjectId();
if (!projectId) throw new Error('Not connected to Firebase');

if (baseUrl) {
const body = { localId: uid };
if (email !== undefined) body.email = email;
if (password !== undefined) body.password = password;
if (displayName !== undefined) body.displayName = displayName;
if (phoneNumber !== undefined) body.phoneNumber = phoneNumber;
if (disabled !== undefined) body.disableUser = disabled;
const response = await fetch(`${baseUrl}/projects/${projectId}/accounts:update`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer owner' },
body: JSON.stringify(body),
});
const data = await response.json();
if (data.error) {
return { success: false, error: data.error.message || 'Update user failed' };
}
const updated = data.localId ? normalizeUserRecord(data) : null;
return {
success: true,
user: updated || { uid, email, displayName, disabled },
};
}

if (!adminRef?.apps?.length) throw new Error('Not connected to Firebase');
const updateData = {};
if (email !== undefined) updateData.email = email;
Expand All @@ -96,9 +210,26 @@ function registerHandlers() {
},
);

// Delete user (Admin SDK)
// Delete user
ipcMain.handle('auth:deleteUser', async (event, { uid }) => {
try {
const baseUrl = await getAuthEmulatorBase();
const projectId = getProjectId();
if (!projectId) throw new Error('Not connected to Firebase');

if (baseUrl) {
const response = await fetch(`${baseUrl}/projects/${projectId}/accounts:delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer owner' },
body: JSON.stringify({ localId: uid }),
});
const data = await response.json();
if (data.error) {
return { success: false, error: data.error.message || 'Delete user failed' };
}
return { success: true };
}

if (!adminRef?.apps?.length) throw new Error('Not connected to Firebase');
await adminRef.auth().deleteUser(uid);
return { success: true };
Expand All @@ -107,9 +238,29 @@ function registerHandlers() {
}
});

// Get user (Admin SDK)
// Get user
ipcMain.handle('auth:getUser', async (event, { uid }) => {
try {
const baseUrl = await getAuthEmulatorBase();
const projectId = getProjectId();
if (!projectId) throw new Error('Not connected to Firebase');

if (baseUrl) {
const response = await fetch(`${baseUrl}/projects/${projectId}/accounts:lookup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer owner' },
body: JSON.stringify({ localId: [uid] }),
});
const data = await response.json();
if (data.error) {
return { success: false, error: data.error.message || 'User not found' };
}
if (!data.users || data.users.length === 0) {
return { success: false, error: 'User not found' };
}
return { success: true, user: normalizeUserRecord(data.users[0]) };
}

if (!adminRef?.apps?.length) throw new Error('Not connected to Firebase');
const userRecord = await adminRef.auth().getUser(uid);
return {
Expand All @@ -135,4 +286,4 @@ function registerHandlers() {
});
}

module.exports = { registerHandlers, setAdminRef };
module.exports = { registerHandlers, setAdminRef, setAuthEmulatorHost };
Loading
Loading