Shave for Hope - Firebase Admin Dashboard Guide
Technical guide for the admin dashboard development team to understand and interact with the Firebase backend.
Firebase Project Info
| Item | Value |
|---|---|
| Project ID | studio-3022188308-abeaa |
| Region | asia-east1 (Hong Kong) |
| Console URL | https://console.firebase.google.com/project/studio-3022188308-abeaa |
| Firestore | https://console.firebase.google.com/project/studio-3022188308-abeaa/firestore |
| Storage | https://console.firebase.google.com/project/studio-3022188308-abeaa/storage |
| Auth | https://console.firebase.google.com/project/studio-3022188308-abeaa/authentication |
Firestore Collections
1. users - Registered Users
interface UserProfile {
uid: string; // Firebase Auth UID (document ID)
displayName: string; // Required
email: string; // From OAuth/registration
avatarUrl?: string; // Profile photo URL
instagramHandle?: string; // e.g., "@username"
instagramPostUrl?: string; // User's shared IG post
fundraisingGoal?: number; // HKD target
personalStory?: string; // Why they support CCF
language: 'zh' | 'en'; // UI language preference
createdAt: Timestamp;
updatedAt: Timestamp;
// Usage quotas (nested objects)
imageQuota?: {
dailyLimit: number; // Default: 4
weeklyLimit: number; // Default: 10
lifetimeLimit: number; // Default: 20
dailyUsed: number;
weeklyUsed: number;
lifetimeUsed: number;
dailyResetAt: Timestamp; // HKT midnight
weeklyResetAt: Timestamp; // Monday HKT midnight
unlimited?: boolean; // Admin override
};
videoQuota?: {
// Same structure, defaults: 2/5/10
};
}
Admin Dashboard Use Cases:
- View all registered users
- Search users by email/displayName
- Modify user quotas (set
unlimited: truefor VIP users) - View user activity statistics
Example Queries:
// Get all users
const users = await getDocs(collection(db, 'users'));
// Get user by UID
const user = await getDoc(doc(db, 'users', uid));
// Search by email
const q = query(collection(db, 'users'), where('email', '==', email));
// Set unlimited quota for a user
await updateDoc(doc(db, 'users', uid), {
'imageQuota.unlimited': true,
'videoQuota.unlimited': true
});
2. transformations - Registered User Transformations
interface Transformation {
id: string; // Auto-generated document ID
userId: string; // Reference to users collection
originalImageUrl: string; // Firebase Storage URL
transformedImageUrl: string; // Firebase Storage URL
videoUrl?: string; // If video was generated
videoStatus?: 'pending' | 'processing' | 'complete' | 'failed';
videoLanguage?: 'zh' | 'en';
shareCount: number; // Times shared
isPublic: boolean; // Visible on public profile
createdAt: Timestamp;
}
Admin Dashboard Use Cases:
- View all transformations with user info
- Filter by date range
- View transformation statistics
- Moderate inappropriate content
Example Queries:
// Get recent transformations
const q = query(
collection(db, 'transformations'),
orderBy('createdAt', 'desc'),
limit(50)
);
// Get transformations by user
const q = query(
collection(db, 'transformations'),
where('userId', '==', uid)
);
// Count total transformations
const snapshot = await getCountFromServer(collection(db, 'transformations'));
console.log(snapshot.data().count);
3. anonymousTransformations - Guest User Transformations
interface AnonymousTransformation {
id: string;
visitorId: string; // Browser UUID
originalImagePath: string; // Storage path
transformedImagePath: string; // Storage path
originalImageUrl: string; // Download URL
transformedImageUrl: string; // Download URL
shareId?: string; // Link to shares collection
createdAt: Timestamp;
expiresAt: Timestamp; // 7 days from creation (for DLC)
}
Note: These are automatically cleaned up after 90 days via Storage lifecycle rules.
4. anonymousQuotas - Guest User Rate Limiting
interface AnonymousQuota {
visitorId: string; // Browser UUID (document ID)
dailyCount: number; // Today's transforms (max 3)
dailyResetAt: Timestamp; // Next HKT midnight
lifetimeCount: number; // Total transforms ever
createdAt: Timestamp;
updatedAt: Timestamp;
}
Admin Dashboard Use Cases:
- Monitor anonymous usage patterns
- Identify potential abuse (high lifetime counts)
- Reset quotas if needed
5. shares - Public Share Pages
interface Share {
id: string; // Short ID used in URL
imageUrl: string; // Full image (~300KB)
ogImageUrl?: string; // OG thumbnail (1200x630, ~150KB)
userId?: string; // If logged in
visitorId?: string; // If anonymous
platform?: string; // whatsapp, facebook, instagram, copy
createdAt: Timestamp;
}
Share Page URL: https://shaveforhope.ccf.org.hk/share/{id}
Admin Dashboard Use Cases:
- View all shares with platform breakdown
- Track viral shares (by view count if implemented)
- Moderate shared content
6. instagramPosts - Community Gallery
interface InstagramPost {
id: string;
postUrl: string; // https://instagram.com/p/ABC123
igUsername: string; // Lowercase IG handle
mediaUrl: string; // Firebase Storage URL (not IG CDN)
mediaType: 'image' | 'video';
caption?: string;
postedAt: Timestamp; // When posted on IG
harvestedAt: Timestamp; // When added to our system
matchedUserId?: string; // Our user ID if matched
source: 'scraper' | 'manual';
}
Admin Dashboard Use Cases:
- Add/remove posts from gallery
- Match posts to registered users
- Moderate inappropriate content
Example: Add a post manually:
await addDoc(collection(db, 'instagramPosts'), {
postUrl: 'https://instagram.com/p/ABC123',
igUsername: 'username',
mediaUrl: 'https://storage.googleapis.com/...',
mediaType: 'image',
caption: 'My shave for hope!',
postedAt: Timestamp.now(),
harvestedAt: Timestamp.now(),
source: 'manual'
});
7. statistics - Global Metrics (Single Document)
// Document ID: 'global'
interface Statistics {
totalUsers: number;
totalImagesAnonymous: number;
totalImagesLoggedIn: number;
totalVideos: number;
totalShares: number;
sharesByPlatform: {
whatsapp: number;
facebook: number;
instagram: number;
copy: number;
};
updatedAt: Timestamp;
}
Admin Dashboard Use Cases:
- Display on admin dashboard homepage
- Generate reports
Example: Read statistics:
const statsDoc = await getDoc(doc(db, 'statistics', 'global'));
const stats = statsDoc.data();
console.log(`Total users: ${stats.totalUsers}`);
Firebase Storage Structure
Firebase Storage Bucket
├── /transformations/{userId}/
│ ├── original_{timestamp}.jpg (~500KB)
│ └── transformed_{timestamp}.jpg (~300KB)
│
├── /anonymous/{visitorId}/
│ ├── original_{timestamp}.jpg (~500KB, 90-day retention)
│ └── transformed_{timestamp}.jpg (~300KB, 90-day retention)
│
├── /public/shares/
│ ├── {timestamp}_{id}.jpg (~300KB, permanent)
│ └── og_{timestamp}_{id}.jpg (~150KB, OG thumbnail)
│
├── /videos/{userId}/
│ └── video_{timestamp}.mp4 (~10-30MB)
│
├── /avatars/{userId}/
│ └── avatar.jpg (~200KB)
│
└── /archive/ (Read-only, managed by DLC)
Storage URLs format:
https://firebasestorage.googleapis.com/v0/b/studio-3022188308-abeaa.firebasestorage.app/o/{path}?alt=media
Security Rules Summary
Firestore Rules
| Collection | Read | Write |
|---|---|---|
users |
Owner only | Owner only |
transformations |
Public if isPublic, else owner |
Owner |
anonymousTransformations |
Public | Create only |
anonymousQuotas |
Public | Public |
shares |
Public | Create only |
instagramPosts |
Public | Admin SDK only |
statistics |
Public | Public (increment) |
Storage Rules
| Path | Read | Write |
|---|---|---|
/transformations/{userId}/ |
Public | Owner, <2MB |
/anonymous/{visitorId}/ |
Public | Anyone, <2MB |
/public/shares/ |
Public | Anyone, <1MB |
/videos/{userId}/ |
Public | Owner, <50MB |
/avatars/{userId}/ |
Public | Owner, <1MB |
/archive/ |
Public | None (DLC only) |
Admin SDK Setup
For the admin dashboard, use Firebase Admin SDK (server-side) to bypass security rules:
// Initialize Admin SDK
const admin = require('firebase-admin');
const serviceAccount = require('./service-account-key.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
storageBucket: 'studio-3022188308-abeaa.firebasestorage.app'
});
const db = admin.firestore();
const storage = admin.storage();
// Now you can read/write any collection
const users = await db.collection('users').get();
Get Service Account Key:
- Go to Firebase Console → Project Settings → Service Accounts
- Click “Generate new private key”
- Store securely (never commit to git!)
Common Admin Operations
1. Get Dashboard Statistics
async function getDashboardStats() {
const [stats, usersCount, transformsCount, sharesCount] = await Promise.all([
db.collection('statistics').doc('global').get(),
db.collection('users').count().get(),
db.collection('transformations').count().get(),
db.collection('shares').count().get(),
]);
return {
...stats.data(),
verifiedUsersCount: usersCount.data().count,
verifiedTransformsCount: transformsCount.data().count,
verifiedSharesCount: sharesCount.data().count,
};
}
2. Search Users
async function searchUsers(searchTerm) {
// Note: Firestore doesn't support full-text search
// For production, consider Algolia or Elasticsearch
// Search by exact email
const byEmail = await db.collection('users')
.where('email', '==', searchTerm)
.get();
// For partial match, fetch all and filter client-side
// (not recommended for large datasets)
}
3. Grant Unlimited Quota
async function grantUnlimitedQuota(uid) {
await db.collection('users').doc(uid).update({
'imageQuota.unlimited': true,
'videoQuota.unlimited': true,
});
}
4. Reset Anonymous Quota
async function resetAnonymousQuota(visitorId) {
await db.collection('anonymousQuotas').doc(visitorId).delete();
}
// Reset all anonymous quotas
async function resetAllAnonymousQuotas() {
const batch = db.batch();
const quotas = await db.collection('anonymousQuotas').get();
quotas.docs.forEach(doc => batch.delete(doc.ref));
await batch.commit();
}
5. Add Instagram Post to Gallery
async function addInstagramPost(postUrl, mediaUrl, username) {
await db.collection('instagramPosts').add({
postUrl,
mediaUrl,
igUsername: username.toLowerCase(),
mediaType: 'image',
postedAt: admin.firestore.Timestamp.now(),
harvestedAt: admin.firestore.Timestamp.now(),
source: 'manual',
});
}
6. Export Data for Reports
async function exportTransformationReport(startDate, endDate) {
const transforms = await db.collection('transformations')
.where('createdAt', '>=', startDate)
.where('createdAt', '<=', endDate)
.orderBy('createdAt', 'desc')
.get();
return transforms.docs.map(doc => ({
id: doc.id,
...doc.data(),
createdAt: doc.data().createdAt.toDate().toISOString(),
}));
}
Environment Variables
For admin dashboard, you’ll need:
# Firebase Admin SDK
FIREBASE_PROJECT_ID=studio-3022188308-abeaa
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@studio-3022188308-abeaa.iam.gserviceaccount.com
# Storage bucket
FIREBASE_STORAGE_BUCKET=studio-3022188308-abeaa.firebasestorage.app
Data Lifecycle & Cleanup
| Data Type | Retention | Cleanup Method |
|---|---|---|
| Anonymous images | 90 days | Storage DLC (auto) |
| Anonymous quotas | Indefinite | Manual or cron |
| Registered user data | Indefinite | Manual on request |
| Share pages | Indefinite | Manual moderation |
| Statistics | Indefinite | N/A |
Useful Links
| *Document Version: 1.0 | Created: 2026-01-09 | Author: Development Team* |