599 lines
19 KiB
TypeScript
599 lines
19 KiB
TypeScript
import { initializeApp, getApp, getApps } from 'firebase/app';
|
|
import {
|
|
getAuth,
|
|
signInWithEmailAndPassword,
|
|
createUserWithEmailAndPassword,
|
|
signOut,
|
|
onAuthStateChanged,
|
|
getUser,
|
|
GoogleAuthProvider,
|
|
signInWithPopup
|
|
} 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<any[]> {
|
|
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 signInWithOAuth({ provider }: { provider: string }) {
|
|
if (provider !== 'google') {
|
|
return { data: { provider: null }, error: new Error("Unsupported OAuth provider") };
|
|
}
|
|
try {
|
|
const googleProvider = new GoogleAuthProvider();
|
|
const credential = await signInWithPopup(auth, googleProvider);
|
|
const user = {
|
|
id: credential.user.uid,
|
|
email: credential.user.email,
|
|
user_metadata: {
|
|
name: credential.user.displayName
|
|
}
|
|
};
|
|
return { data: { user }, error: null };
|
|
} catch (error: any) {
|
|
console.error("Firebase Google Auth Signin Error:", error);
|
|
return { data: { user: null }, error };
|
|
}
|
|
},
|
|
|
|
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<any, FirestoreChannel>();
|
|
|
|
// 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<string> => {
|
|
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);
|
|
}
|
|
})(); |