Files
Event-Management-System/RIT-EVENT-MANAGEMENT--main/supabase.ts

576 lines
18 KiB
TypeScript

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<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 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);
}
})();