import { initializeApp, getApp, getApps } from 'firebase/app'; import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged, getUser } from 'firebase/auth'; import { getFirestore, collection, getDocs, doc, getDoc, setDoc, updateDoc, deleteDoc, query, where, orderBy, limit, onSnapshot } from 'firebase/firestore'; import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage'; // Firebase Configuration from user const firebaseConfig = { apiKey: "AIzaSyBdRUyA7LDtDReUA3TXDys71dSHgD2tOEA", authDomain: "ems-ritchennai1.firebaseapp.com", projectId: "ems-ritchennai1", storageBucket: "ems-ritchennai1.firebasestorage.app", messagingSenderId: "825363154108", appId: "1:825363154108:web:7b6d3430aa3b696fef3cb9", measurementId: "G-3W7ECQ52G1" }; // Initialize main Firebase App const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp(); const auth = getAuth(app); const db = getFirestore(app); const storage = getStorage(app); /** * Real-time channel handler mapping Supabase real-time channels to Firestore onSnapshot */ class FirestoreChannel { private name: string; private listeners: (() => void)[] = []; constructor(name: string) { this.name = name; } on(event: string, filter: { event: string; schema: string; table: string; filter?: string }, callback: () => void) { const table = filter.table; const colRef = collection(db, table); let q: any = colRef; if (filter.filter) { const parts = filter.filter.split('=eq.'); if (parts.length === 2) { const field = parts[0]; const val = parts[1]; q = query(colRef, where(field, '==', val)); } } const unsubscribe = onSnapshot(q, () => { callback(); }, (err) => { console.warn(`Firestore Channel ${this.name} snapshot error:`, err); }); this.listeners.push(unsubscribe); return this; } subscribe() { return this; } unsubscribeAll() { this.listeners.forEach(unsub => unsub()); this.listeners = []; } } /** * Firestore Query Builder adapter representing Supabase's from('table') syntax */ class FirestoreQueryBuilder { private table: string; private filters: { field: string; op: any; val: any }[] = []; private orderByField: string | null = null; private orderByDirection: 'asc' | 'desc' = 'asc'; private limitCount: number | null = null; private isSingle = false; private selectFields = '*'; private operation: 'select' | 'insert' | 'update' | 'delete' | 'upsert' = 'select'; private payload: any = null; constructor(table: string) { this.table = table; } select(fields = '*') { this.selectFields = fields; this.operation = 'select'; return this; } insert(data: any) { this.operation = 'insert'; this.payload = data; return this; } update(data: any) { this.operation = 'update'; this.payload = data; return this; } delete() { this.operation = 'delete'; return this; } upsert(data: any) { this.operation = 'upsert'; this.payload = data; return this; } eq(field: string, val: any) { this.filters.push({ field, op: '==', val }); return this; } neq(field: string, val: any) { this.filters.push({ field, op: '!=', val }); return this; } gt(field: string, val: any) { this.filters.push({ field, op: '>', val }); return this; } gte(field: string, val: any) { this.filters.push({ field, op: '>=', val }); return this; } lt(field: string, val: any) { this.filters.push({ field, op: '<', val }); return this; } lte(field: string, val: any) { this.filters.push({ field, op: '<=', val }); return this; } in(field: string, val: any[]) { this.filters.push({ field, op: 'in', val }); return this; } order(field: string, options?: { ascending: boolean }) { this.orderByField = field; this.orderByDirection = options?.ascending === false ? 'desc' : 'asc'; return this; } limit(count: number) { this.limitCount = count; return this; } single() { this.isSingle = true; return this; } maybeSingle() { this.isSingle = true; return this; } private async fetchDocs(): Promise { const colRef = collection(db, this.table); let qConstraints: any[] = []; for (const filter of this.filters) { qConstraints.push(where(filter.field, filter.op, filter.val)); } if (this.orderByField) { qConstraints.push(orderBy(this.orderByField, this.orderByDirection)); } if (this.limitCount !== null) { qConstraints.push(limit(this.limitCount)); } const q = query(colRef, ...qConstraints); const snap = await getDocs(q); return snap.docs.map(d => d.data()); } async execute() { try { if (this.operation === 'insert') { const colRef = collection(db, this.table); const items = Array.isArray(this.payload) ? this.payload : [this.payload]; const insertedItems: any[] = []; for (const item of items) { const docId = item.id || item.user_id || doc(colRef).id; const docRef = doc(db, this.table, String(docId)); const finalItem = { ...item, id: docId }; await setDoc(docRef, finalItem); insertedItems.push(finalItem); } const data = Array.isArray(this.payload) ? insertedItems : insertedItems[0]; return { data: this.isSingle && Array.isArray(data) ? data[0] : data, error: null }; } if (this.operation === 'update') { const docsToUpdate = await this.fetchDocs(); const updatedItems: any[] = []; for (const docObj of docsToUpdate) { const docId = docObj.id || docObj.user_id; if (!docId) continue; const docRef = doc(db, this.table, String(docId)); await updateDoc(docRef, this.payload); updatedItems.push({ ...docObj, ...this.payload }); } const data = this.isSingle && updatedItems.length > 0 ? updatedItems[0] : updatedItems; return { data, error: null }; } if (this.operation === 'delete') { const docsToDelete = await this.fetchDocs(); for (const docObj of docsToDelete) { const docId = docObj.id || docObj.user_id; if (!docId) continue; const docRef = doc(db, this.table, String(docId)); await deleteDoc(docRef); } const data = this.isSingle && docsToDelete.length > 0 ? docsToDelete[0] : docsToDelete; return { data, error: null }; } if (this.operation === 'upsert') { const items = Array.isArray(this.payload) ? this.payload : [this.payload]; const result: any[] = []; for (const item of items) { const docId = item.id || item.user_id || doc(collection(db, this.table)).id; const docRef = doc(db, this.table, String(docId)); const finalItem = { ...item, id: docId }; await setDoc(docRef, finalItem, { merge: true }); result.push(finalItem); } const data = Array.isArray(this.payload) ? result : result[0]; return { data: this.isSingle && Array.isArray(data) ? data[0] : data, error: null }; } // Default: select let list = await this.fetchDocs(); // Resolve joins if requested. E.g. select('*, events(title, category)') if (this.selectFields && this.selectFields.includes('events(')) { for (const item of list) { if (item.event_id) { const evRef = doc(db, 'events', String(item.event_id)); const evSnap = await getDoc(evRef); if (evSnap.exists()) { const evData = evSnap.data(); item.events = { title: evData.title, category: evData.category }; } else { item.events = null; } } } } if (this.isSingle) { if (list.length === 0) { return { data: null, error: { message: 'Document not found', code: 'PGRST116' } }; } return { data: list[0], error: null }; } return { data: list, error: null }; } catch (err: any) { console.error(`Error in ${this.operation} on ${this.table}:`, err); return { data: null, error: err }; } } then(onfulfilled?: (value: any) => any, onrejected?: (reason: any) => any) { return this.execute().then(onfulfilled, onrejected); } } // Map supabase.auth const supabaseAuth = { async signInWithPassword({ email, password }: any) { try { const credential = await signInWithEmailAndPassword(auth, email, password); const user = { id: credential.user.uid, email: credential.user.email, user_metadata: {} }; return { data: { user }, error: null }; } catch (error: any) { console.error("Firebase Auth Signin Error:", error); return { data: { user: null }, error }; } }, async signUp({ email, password, options }: any) { try { const credential = await createUserWithEmailAndPassword(auth, email, password); const user = { id: credential.user.uid, email: credential.user.email, user_metadata: options?.data || {} }; return { data: { user }, error: null }; } catch (error: any) { console.error("Firebase Auth Signup Error:", error); return { data: { user: null }, error }; } }, async signOut() { try { await signOut(auth); return { error: null }; } catch (error: any) { return { error }; } }, async getUser() { const currentUser = auth.currentUser; if (currentUser) { return { data: { user: { id: currentUser.uid, email: currentUser.email } }, error: null }; } return { data: { user: null }, error: null }; }, async getSession() { const currentUser = auth.currentUser; if (currentUser) { return { data: { session: { user: { id: currentUser.uid, email: currentUser.email } } }, error: null }; } return { data: { session: null }, error: null }; }, onAuthStateChange(callback: (event: string, session: any) => void) { const unsubscribe = onAuthStateChanged(auth, (user) => { if (user) { callback('SIGNED_IN', { user: { id: user.uid, email: user.email } }); } else { callback('SIGNED_OUT', null); } }); return { data: { subscription: { unsubscribe } } }; } }; // Map active channels const activeChannels = new Map(); // Expose main supabase client export const supabase = { auth: supabaseAuth, from(table: string) { return new FirestoreQueryBuilder(table); }, channel(name: string) { const chan = new FirestoreChannel(name); activeChannels.set(chan, chan); return chan; }, removeChannel(chan: any) { if (chan && typeof chan.unsubscribeAll === 'function') { chan.unsubscribeAll(); activeChannels.delete(chan); } }, storage: { from(bucket: string) { return { upload(path: string, blob: Blob, options?: any) { return new Promise(async (resolve, reject) => { try { const storageRef = ref(storage, `${bucket}/${path}`); const snap = await uploadBytes(storageRef, blob, { contentType: options?.contentType || blob.type }); resolve({ data: snap, error: null }); } catch (error) { resolve({ data: null, error }); } }); }, getPublicUrl(path: string) { const storageRef = ref(storage, `${bucket}/${path}`); return { data: { publicUrl: `https://firebasestorage.googleapis.com/v0/b/${firebaseConfig.storageBucket}/o/${encodeURIComponent(`${bucket}/${path}`)}?alt=media` } }; } }; } } }; /** * Creates a secondary App instance for administrative user signups. * This prevents the administrator's session from being overwritten. */ export const createAdminClient = () => { const adminAppName = 'AdminAuthApp'; let adminApp; if (getApps().some(app => app.name === adminAppName)) { adminApp = getApp(adminAppName); } else { adminApp = initializeApp(firebaseConfig, adminAppName); } const adminAuth = getAuth(adminApp); return { auth: { async signUp({ email, password }: any) { try { const credential = await createUserWithEmailAndPassword(adminAuth, email, password); // Immediately sign out from the admin app so we don't store session await signOut(adminAuth); return { data: { user: { id: credential.user.uid, email: credential.user.email } }, error: null }; } catch (error: any) { console.error("Firebase Admin Auth Signup Error:", error); return { data: { user: null }, error }; } } } }; }; /** * Base64 upload helper mapping to Firebase Storage */ export const uploadToSupabase = async ( base64String: string, path: string, bucket: string = 'rit-events' ): Promise => { try { const parts = base64String.split(';base64,'); if (parts.length < 2) throw new Error('Invalid base64 string'); const contentType = parts[0].split(':')[1]; const raw = window.atob(parts[1]); const rawLength = raw.length; const uInt8Array = new Uint8Array(rawLength); for (let i = 0; i < rawLength; ++i) { uInt8Array[i] = raw.charCodeAt(i); } const blob = new Blob([uInt8Array], { type: contentType }); const storageRef = ref(storage, `${bucket}/${path}`); await uploadBytes(storageRef, blob, { contentType }); const publicUrl = await getDownloadURL(storageRef); return publicUrl; } catch (error) { console.error('Firebase Storage Upload Error:', error); throw error; } }; export const uploadImageToSupabase = uploadToSupabase; // Automatic Seeder for domains if empty (async () => { try { const colRef = collection(db, 'domains'); const snap = await getDocs(colRef); if (snap.empty) { console.log('Domains collection is empty. Seeding initial domains...'); const DOMAIN_MAP = { 'TECHNICAL': [ { name: 'CSE', description: 'Computer Science and Engineering', image: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'CSBS', description: 'Computer Science and Business Systems', image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'AIML', description: 'Artificial Intelligence and Machine Learning (AIML)', image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'AIDS', description: 'AIDS', image: 'https://images.unsplash.com/photo-1509228627152-72ae9ae6848d?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'IT', description: 'Information Technology', image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'ECE', description: 'ECE', image: 'https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'Mechanical', description: 'Mechanical', image: 'https://images.unsplash.com/photo-1537462715879-360eeb61a0ad?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'Bio-tech', description: 'Bio-tech', image: 'https://images.unsplash.com/photo-1530210124550-912dc1381cb8?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'CCE', description: 'CCE', image: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' }, { name: 'VLSI', description: 'VLSI', image: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&q=80&w=600', status: 'APPROVED', category: 'TECHNICAL' } ], 'NON-TECHNICAL': [ { name: 'Management', description: 'Management', image: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'NON-TECHNICAL' }, { name: 'Arts & Culture', description: 'Arts & Culture', image: 'https://images.unsplash.com/photo-1513364776144-60967b0f800f?auto=format&fit=crop&q=80&w=800', status: 'APPROVED', category: 'NON-TECHNICAL' }, { name: 'Sports', description: 'Sports & Athletics', image: 'https://images.unsplash.com/photo-1552674605-db6ffd4facb5?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'NON-TECHNICAL' }, { name: 'Social Welfare', description: 'Social Service', image: 'https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'NON-TECHNICAL' } ], 'WORKSHOP': [ { name: 'Software Dev', description: 'Software Engineering', image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'WORKSHOP' }, { name: 'Design', description: 'UI/UX Design', image: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?auto=format&fit=crop&q=80&w=800', status: 'APPROVED', category: 'WORKSHOP' }, { name: 'Cloud/DevOps', description: 'Cloud', image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=400', status: 'APPROVED', category: 'WORKSHOP' } ] }; for (const [_, domains] of Object.entries(DOMAIN_MAP)) { for (const domain of domains) { const docId = doc(colRef).id; const docRef = doc(db, 'domains', docId); await setDoc(docRef, { ...domain, id: docId }); } } console.log('Seeded domains successfully.'); } } catch (err) { console.error('Automatic domains seeder failed:', err); } })();