diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..830be13 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Supabase Configuration +VITE_SUPABASE_URL=your_supabase_url_here +VITE_SUPABASE_ANON_KEY=your_supabase_anon_key_here + +# Gemini AI API Key +VITE_GEMINI_API_KEY=your_gemini_api_key_here + +# Mock API Passwords (for development/testing only) +# These are only used when mockApi.ts is active +VITE_ADMIN_PASSWORD=your_admin_password_here +VITE_USER_PASSWORD=your_user_password_here diff --git a/.gitignore b/.gitignore index a547bf3..556cd6e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ dist dist-ssr *.local +# Environment variables +.env +.env.local +.env.*.local + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/DATABASE_SCHEMA.md b/DATABASE_SCHEMA.md index 960c9f7..24fdbfc 100644 --- a/DATABASE_SCHEMA.md +++ b/DATABASE_SCHEMA.md @@ -242,6 +242,68 @@ CREATE POLICY "Users can delete own moments" ON moments FOR DELETE USING (auth.uid() = user_id); ``` +<<<<<<< HEAD +### 7. `transactions` table + +Stores transaction history for payment tracking. + +```sql +CREATE TABLE transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + spot_id UUID NOT NULL REFERENCES spots(id) ON DELETE CASCADE, + amount NUMERIC NOT NULL, + payment_method TEXT NOT NULL DEFAULT 'UPI', + status TEXT NOT NULL DEFAULT 'not_paid' CHECK (status IN ('paid', 'not_paid')), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create indexes for faster queries +CREATE INDEX transactions_user_id_idx ON transactions(user_id); +CREATE INDEX transactions_spot_id_idx ON transactions(spot_id); +CREATE INDEX transactions_created_at_idx ON transactions(created_at DESC); + +-- Enable RLS +ALTER TABLE transactions ENABLE ROW LEVEL SECURITY; + +-- Policy: Users can read their own transactions +CREATE POLICY "Users can read own transactions" ON transactions + FOR SELECT USING (auth.uid() = user_id); + +-- Policy: Admins can read all transactions +CREATE POLICY "Admins can read all transactions" ON transactions + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM profiles + WHERE profiles.id = auth.uid() + AND profiles.role = 'admin' + ) + ); + +-- Policy: Admins can create transactions +CREATE POLICY "Admins can create transactions" ON transactions + FOR INSERT WITH CHECK ( + EXISTS ( + SELECT 1 FROM profiles + WHERE profiles.id = auth.uid() + AND profiles.role = 'admin' + ) + ); + +-- Policy: Admins can update transactions +CREATE POLICY "Admins can update transactions" ON transactions + FOR UPDATE USING ( + EXISTS ( + SELECT 1 FROM profiles + WHERE profiles.id = auth.uid() + AND profiles.role = 'admin' + ) + ); +``` + +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b ## Real-time Subscriptions Enable real-time for the following tables in Supabase Dashboard: @@ -249,6 +311,10 @@ Enable real-time for the following tables in Supabase Dashboard: - `invitations` - for RSVP updates - `payments` - for payment status updates - `chat_messages` - for chat updates +<<<<<<< HEAD +- `transactions` - for transaction history updates +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b ## Initial Data diff --git a/SECURITY_SETUP.md b/SECURITY_SETUP.md new file mode 100644 index 0000000..65d4b49 --- /dev/null +++ b/SECURITY_SETUP.md @@ -0,0 +1,109 @@ +# Security Setup Guide + +## ✅ Security Fixes Applied + +### 1. Environment Variables +All sensitive data (passwords, API keys) moved to `.env` file. + +### 2. Files Created +- `.env` - Contains actual secrets (NOT in git) +- `.env.example` - Template for other developers (in git) +- `.gitignore` - Updated to exclude `.env` files + +### 3. Mock API Updated +`services/mockApi.ts` now uses environment variables instead of hardcoded passwords. + +--- + +## 🔐 How It Works + +**Before (Insecure):** +```typescript +password: "admin@brocode" // ❌ Hardcoded in code +``` + +**After (Secure):** +```typescript +password: import.meta.env.VITE_ADMIN_PASSWORD || "changeme" // ✅ From .env +``` + +--- + +## 📝 Setup Instructions + +### For New Developers: + +1. **Copy the example file:** + ```bash + copy .env.example .env + ``` + +2. **Edit `.env` and add your credentials:** + ```env + VITE_ADMIN_PASSWORD=your_password_here + VITE_USER_PASSWORD=your_password_here + ``` + +3. **Never commit `.env` to git!** + - It's already in `.gitignore` + - Only commit `.env.example` + +--- + +## 🚨 Important Notes + +### Mock API (Development Only) +- `services/mockApi.ts` is only for local testing +- Production uses Supabase (real database) +- Mock passwords are safe because they're not in production + +### Production Security +- Real passwords are in Supabase database +- Supabase handles authentication securely +- No passwords stored in frontend code + +### GitGuardian Warnings +- After this fix, GitGuardian warnings will stop +- Old commits may still show warnings (that's okay) +- New commits will be clean + +--- + +## 🔄 Migration from Old Code + +If you have old code with hardcoded passwords: + +1. Pull latest changes +2. Create `.env` file from `.env.example` +3. Add your passwords to `.env` +4. Restart dev server: `npm run dev` + +--- + +## ✨ Best Practices + +✅ **DO:** +- Use environment variables for secrets +- Keep `.env` in `.gitignore` +- Share `.env.example` with team +- Use different passwords for dev/prod + +❌ **DON'T:** +- Commit `.env` to git +- Share passwords in code +- Use same password everywhere +- Hardcode API keys + +--- + +## 🛡️ Security Checklist + +- [x] Passwords moved to environment variables +- [x] `.env` added to `.gitignore` +- [x] `.env.example` created for team +- [x] Mock API updated to use env vars +- [x] Documentation created + +--- + +Need help? Check `.env.example` for required variables! diff --git a/TRANSACTION_SETUP_GUIDE.md b/TRANSACTION_SETUP_GUIDE.md new file mode 100644 index 0000000..ca85466 --- /dev/null +++ b/TRANSACTION_SETUP_GUIDE.md @@ -0,0 +1,93 @@ +# Transaction History Setup Guide + +## Quick Setup (2 minutes) + +### Option 1: Supabase Dashboard (Recommended - Easiest) + +1. **Open Supabase Dashboard** + - Go to: https://supabase.com + - Login and select your project + +2. **Open SQL Editor** + - Click "SQL Editor" in the left sidebar + - Click "New Query" button + +3. **Copy & Paste** + - Open the file: `supabase_migration_transactions.sql` + - Copy ALL the content (Ctrl+A, Ctrl+C) + - Paste in Supabase SQL Editor (Ctrl+V) + +4. **Run** + - Click "Run" button (or press Ctrl+Enter) + - Wait for success message + +5. **Done!** + - Refresh your app + - Transaction history will now appear on Payment page + +--- + +## Option 2: Using Supabase CLI (If you want to install CLI) + +### Install Supabase CLI: +```powershell +# Using Scoop (recommended for Windows) +scoop bucket add supabase https://github.com/supabase/scoop-bucket.git +scoop install supabase + +# OR using npm +npm install -g supabase +``` + +### Run Migration: +```powershell +# Link your project (one time only) +supabase link --project-ref your-project-ref + +# Run migration +supabase db push +``` + +--- + +## Verify Installation + +After running the SQL: + +1. Go to Supabase Dashboard > Database > Tables +2. You should see a new table called `transactions` +3. Open your app and go to Payment page +4. You should see "Transaction History" section at the bottom + +--- + +## Troubleshooting + +**If you see "table does not exist" error:** +- The migration hasn't been run yet +- Go back to Option 1 and run the SQL in dashboard + +**If transaction history is empty:** +- That's normal! Transactions will appear when: + - Admin marks a payment as "Paid" + - New payments are processed + +**If you see permission errors:** +- Make sure you're logged in as admin +- Check RLS policies in Supabase Dashboard + +--- + +## What This Feature Does + +✅ Shows complete payment history +✅ Displays: Date, Amount, Payment Method, Status +✅ Users see only their own transactions +✅ Admins see all transactions +✅ Automatic transaction creation when payment is marked paid +✅ Mobile responsive design + +--- + +Need help? The SQL file is ready at: `supabase_migration_transactions.sql` +Just copy-paste it in Supabase SQL Editor and click Run! diff --git a/components/common/TransactionHistory.tsx b/components/common/TransactionHistory.tsx new file mode 100644 index 0000000..32e18bf --- /dev/null +++ b/components/common/TransactionHistory.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { Transaction, PaymentStatus } from "../../types"; +import Card from "./Card"; + +interface TransactionHistoryProps { + transactions: Transaction[]; + loading?: boolean; +} + +const TransactionHistory: React.FC = ({ + transactions, + loading = false, +}) => { + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString("en-IN", { + day: "2-digit", + month: "short", + year: "numeric", + }); + }; + + const getStatusBadge = (status: PaymentStatus) => { + const isPaid = status === PaymentStatus.PAID; + return ( + + {isPaid ? "Paid" : "Pending"} + + ); + }; + + if (loading) { + return ( + +

+ Transaction History +

+
Loading...
+
+ ); + } + + return ( + +

+ Transaction History +

+ + {transactions.length === 0 ? ( +

+ No transactions found. +

+ ) : ( +
+ + + + + + + + + + + {transactions.map((transaction) => ( + + + + + + + ))} + +
+ Date + + Amount + + Payment Method + + Status +
+ {formatDate(transaction.created_at)} + + ₹{transaction.amount.toFixed(2)} + + {transaction.payment_method} + + {getStatusBadge(transaction.status)} +
+
+ )} +
+ ); +}; + +export default TransactionHistory; diff --git a/index.css b/index.css new file mode 100644 index 0000000..910a6fb --- /dev/null +++ b/index.css @@ -0,0 +1,29 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: #000000; + color: #e5e7eb; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +#root { + min-height: 100vh; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 44375f8..f63a259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,10 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", +<<<<<<< HEAD + "peer": true, +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1283,6 +1287,10 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", +<<<<<<< HEAD + "peer": true, +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b "dependencies": { "csstype": "^3.2.2" } @@ -1412,6 +1420,10 @@ } ], "license": "MIT", +<<<<<<< HEAD + "peer": true, +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2169,6 +2181,10 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", +<<<<<<< HEAD + "peer": true, +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b "engines": { "node": ">=12" }, @@ -2225,6 +2241,10 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", +<<<<<<< HEAD + "peer": true, +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b "engines": { "node": ">=0.10.0" } @@ -2234,6 +2254,10 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", +<<<<<<< HEAD + "peer": true, +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b "dependencies": { "scheduler": "^0.27.0" }, @@ -2482,6 +2506,10 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", +<<<<<<< HEAD + "peer": true, +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/pages/PaymentPage.tsx b/pages/PaymentPage.tsx index 8aefbfb..53933af 100644 --- a/pages/PaymentPage.tsx +++ b/pages/PaymentPage.tsx @@ -1,5 +1,15 @@ import React, { useEffect, useState, useCallback } from "react"; import { useAuth } from "../hooks/useAuth"; +<<<<<<< HEAD +import { UserRole, Spot, Payment, PaymentStatus, Transaction } from "../types"; +import Card from "../components/common/Card"; +import Button from "../components/common/Button"; +import { QRCodeCanvas } from "qrcode.react"; +import { spotService, paymentService, invitationService, transactionService } from "../services/database"; +import { InvitationStatus } from "../types"; +import { supabase } from "../services/supabase"; +import TransactionHistory from "../components/common/TransactionHistory"; +======= import { UserRole, Spot, Payment, PaymentStatus } from "../types"; import Card from "../components/common/Card"; import Button from "../components/common/Button"; @@ -7,6 +17,7 @@ import { QRCodeCanvas } from "qrcode.react"; import { spotService, paymentService, invitationService } from "../services/database"; import { InvitationStatus } from "../types"; import { supabase } from "../services/supabase"; +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b /* -------------------------------------------------------------------------- */ /* Status Badge */ @@ -31,6 +42,10 @@ const PaymentPage: React.FC = () => { const { profile } = useAuth(); const [spot, setSpot] = useState(null); const [payments, setPayments] = useState([]); +<<<<<<< HEAD + const [transactions, setTransactions] = useState([]); +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b const [loading, setLoading] = useState(true); const fetchData = useCallback(async () => { @@ -73,12 +88,25 @@ const PaymentPage: React.FC = () => { } else { setPayments([]); } +<<<<<<< HEAD + + // Fetch transaction history for current user + if (profile?.id) { + const transactionData = await transactionService.getUserTransactions(profile.id); + setTransactions(transactionData); + } +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b } catch (error) { console.error("Error loading payment data:", error); } finally { setLoading(false); } +<<<<<<< HEAD + }, [profile?.id]); +======= }, []); +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b useEffect(() => { fetchData(); @@ -131,6 +159,22 @@ const PaymentPage: React.FC = () => { if (!isAdmin) return; try { await paymentService.updatePaymentStatus(paymentId, PaymentStatus.PAID); +<<<<<<< HEAD + + // Create a transaction record when payment is marked as paid + const payment = payments.find(p => p.id === paymentId); + if (payment && spot) { + await transactionService.createTransaction({ + user_id: payment.user_id, + spot_id: spot.id, + amount: spot.budget, + payment_method: 'UPI', + status: PaymentStatus.PAID, + }); + } + +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b await fetchData(); } catch (error: any) { alert(`Failed to update payment: ${error.message || 'Please try again.'}`); @@ -298,6 +342,12 @@ const PaymentPage: React.FC = () => { )} +<<<<<<< HEAD + + {/* ---------------- TRANSACTION HISTORY ---------------- */} + +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b ); }; diff --git a/services/database.ts b/services/database.ts index 8f626bc..8c2d650 100644 --- a/services/database.ts +++ b/services/database.ts @@ -1613,3 +1613,137 @@ export const attendanceService = { }; }, }; +<<<<<<< HEAD + +/* TRANSACTIONS */ +/* -------------------------------------------------------------------------- */ + +export const transactionService = { + // Get transactions for a user + async getUserTransactions(userId: string): Promise { + const { data, error } = await supabase + .from('transactions') + .select(` + *, + profiles:user_id ( + id, + name, + username, + profile_pic_url + ), + spots:spot_id ( + id, + date, + location + ) + `) + .eq('user_id', userId) + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching user transactions:', error); + // If table doesn't exist, return empty array + if (error.message?.includes('does not exist') || + error.message?.includes('relation') || + error.code === '42P01') { + return []; + } + throw error; + } + + return data || []; + }, + + // Get all transactions (admin only) + async getAllTransactions(): Promise { + const { data, error } = await supabase + .from('transactions') + .select(` + *, + profiles:user_id ( + id, + name, + username, + profile_pic_url + ), + spots:spot_id ( + id, + date, + location + ) + `) + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching all transactions:', error); + // If table doesn't exist, return empty array + if (error.message?.includes('does not exist') || + error.message?.includes('relation') || + error.code === '42P01') { + return []; + } + throw error; + } + + return data || []; + }, + + // Create a transaction + async createTransaction(transactionData: { + user_id: string; + spot_id: string; + amount: number; + payment_method: string; + status: PaymentStatus; + }): Promise { + const { data, error } = await supabase + .from('transactions') + .insert({ + user_id: transactionData.user_id, + spot_id: transactionData.spot_id, + amount: transactionData.amount, + payment_method: transactionData.payment_method, + status: transactionData.status, + }) + .select(` + *, + profiles:user_id ( + id, + name, + username, + profile_pic_url + ), + spots:spot_id ( + id, + date, + location + ) + `) + .single(); + + if (error) { + console.error('Error creating transaction:', error); + throw error; + } + + return data; + }, + + // Update transaction status + async updateTransactionStatus( + transactionId: string, + status: PaymentStatus + ): Promise { + const { error } = await supabase + .from('transactions') + .update({ status }) + .eq('id', transactionId); + + if (error) { + console.error('Error updating transaction status:', error); + throw error; + } + }, +}; +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b diff --git a/services/mockApi.ts b/services/mockApi.ts index 498dda0..975834d 100644 --- a/services/mockApi.ts +++ b/services/mockApi.ts @@ -41,7 +41,7 @@ let USERS_DB: Record = { username: "brocode", email: "brocode@gmail.com", phone: "7826821130", - password: "admin@brocode", + password: import.meta.env.VITE_ADMIN_PASSWORD || "changeme", role: UserRole.ADMIN, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[0], @@ -52,7 +52,7 @@ let USERS_DB: Record = { name: "Dhanush", username: "dhanush", phone: "9994323520", - password: "dhanush123", + password: import.meta.env.VITE_USER_PASSWORD || "changeme", role: UserRole.USER, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[1], @@ -63,7 +63,7 @@ let USERS_DB: Record = { name: "Godwin", username: "godwin", phone: "8903955341", - password: "godwin123", + password: import.meta.env.VITE_USER_PASSWORD || "changeme", role: UserRole.USER, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[2], @@ -74,7 +74,7 @@ let USERS_DB: Record = { name: "Tharun", username: "tharun", phone: "9345624112", - password: "tharun123", + password: import.meta.env.VITE_USER_PASSWORD || "changeme", role: UserRole.USER, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[3], @@ -85,7 +85,7 @@ let USERS_DB: Record = { name: "Sanjay", username: "sanjay", phone: "9865703667", - password: "sanjay123", + password: import.meta.env.VITE_USER_PASSWORD || "changeme", role: UserRole.USER, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[0], @@ -96,7 +96,7 @@ let USERS_DB: Record = { name: "Soundar", username: "soundar", phone: "9566686921", - password: "soundar123", + password: import.meta.env.VITE_USER_PASSWORD || "changeme", role: UserRole.USER, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[1], @@ -107,7 +107,7 @@ let USERS_DB: Record = { name: "Jagadeesh", username: "jagadeesh", phone: "6381038172", - password: "jagadeesh123", + password: import.meta.env.VITE_USER_PASSWORD || "changeme", role: UserRole.USER, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[2], @@ -118,7 +118,7 @@ let USERS_DB: Record = { name: "Ram", username: "ram", phone: "7826821130", - password: "ram123", + password: import.meta.env.VITE_USER_PASSWORD || "changeme", role: UserRole.USER, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[3], @@ -129,7 +129,7 @@ let USERS_DB: Record = { name: "Lingesh", username: "lingesh", phone: "", - password: "lingesh123", + password: import.meta.env.VITE_USER_PASSWORD || "changeme", role: UserRole.USER, location: "Attibele", profile_pic_url: DEFAULT_AVATARS[0], @@ -306,9 +306,9 @@ export const mockApi = { const profile = Object.values(USERS_DB).find( (u) => - (u.email === identifier || - u.phone === cleanIdentifier || - u.username === identifier) && + (u.email === identifier || + u.phone === cleanIdentifier || + u.username === identifier) && u.password === password ); @@ -538,8 +538,8 @@ export const mockApi = { async getUserSpots(userId: string): Promise { await delay(200); - return SPOTS.filter((s) => - s.created_by === userId || + return SPOTS.filter((s) => + s.created_by === userId || s.members?.some((m) => m.id === userId) ); }, diff --git a/supabase_migration_complete.sql b/supabase_migration_complete.sql new file mode 100644 index 0000000..b22c9a0 --- /dev/null +++ b/supabase_migration_complete.sql @@ -0,0 +1,225 @@ +-- BroCode Complete Database Migration (Including Transactions) +-- Run this SQL in your Supabase SQL Editor +-- This will create all tables including the new transactions table + +-- 1. Create profiles table +CREATE TABLE IF NOT EXISTS profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + username TEXT UNIQUE NOT NULL, + email TEXT, + phone TEXT, + password TEXT, + role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin', 'user', 'guest')), + profile_pic_url TEXT, + location TEXT, + date_of_birth TEXT, + is_verified BOOLEAN DEFAULT false, + latitude NUMERIC, + longitude NUMERIC, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS profiles_username_unique ON profiles(username); +ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Users can read all profiles" ON profiles; +DROP POLICY IF EXISTS "Users can update own profile" ON profiles; +DROP POLICY IF EXISTS "Anyone can insert profiles" ON profiles; + +CREATE POLICY "Users can read all profiles" ON profiles FOR SELECT USING (true); +CREATE POLICY "Users can update own profile" ON profiles FOR UPDATE USING (true); +CREATE POLICY "Anyone can insert profiles" ON profiles FOR INSERT WITH CHECK (true); + +-- 2. Create spots table +CREATE TABLE IF NOT EXISTS spots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + date TIMESTAMP WITH TIME ZONE NOT NULL, + day TEXT NOT NULL, + timing TEXT NOT NULL, + budget NUMERIC NOT NULL DEFAULT 0, + location TEXT NOT NULL, + created_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + description TEXT, + feedback TEXT, + latitude NUMERIC, + longitude NUMERIC, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS spots_date_idx ON spots(date); +CREATE INDEX IF NOT EXISTS spots_created_by_idx ON spots(created_by); +ALTER TABLE spots ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Everyone can read spots" ON spots; +DROP POLICY IF EXISTS "Anyone can create spots" ON spots; +DROP POLICY IF EXISTS "Anyone can update spots" ON spots; +DROP POLICY IF EXISTS "Anyone can delete spots" ON spots; + +CREATE POLICY "Everyone can read spots" ON spots FOR SELECT USING (true); +CREATE POLICY "Anyone can create spots" ON spots FOR INSERT WITH CHECK (true); +CREATE POLICY "Anyone can update spots" ON spots FOR UPDATE USING (true); +CREATE POLICY "Anyone can delete spots" ON spots FOR DELETE USING (true); + +-- 3. Create invitations table +CREATE TABLE IF NOT EXISTS invitations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + spot_id UUID NOT NULL REFERENCES spots(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('confirmed', 'pending', 'declined')), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(spot_id, user_id) +); + +CREATE INDEX IF NOT EXISTS invitations_spot_id_idx ON invitations(spot_id); +CREATE INDEX IF NOT EXISTS invitations_user_id_idx ON invitations(user_id); +ALTER TABLE invitations ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Everyone can read invitations" ON invitations; +DROP POLICY IF EXISTS "Users can manage own invitations" ON invitations; +DROP POLICY IF EXISTS "Anyone can create invitations" ON invitations; + +CREATE POLICY "Everyone can read invitations" ON invitations FOR SELECT USING (true); +CREATE POLICY "Users can manage own invitations" ON invitations FOR ALL USING (true); +CREATE POLICY "Anyone can create invitations" ON invitations FOR INSERT WITH CHECK (true); + +-- 4. Create payments table +CREATE TABLE IF NOT EXISTS payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + spot_id UUID NOT NULL REFERENCES spots(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + status TEXT NOT NULL DEFAULT 'not_paid' CHECK (status IN ('paid', 'not_paid')), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(spot_id, user_id) +); + +CREATE INDEX IF NOT EXISTS payments_spot_id_idx ON payments(spot_id); +CREATE INDEX IF NOT EXISTS payments_user_id_idx ON payments(user_id); +ALTER TABLE payments ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Everyone can read payments" ON payments; +DROP POLICY IF EXISTS "Anyone can update payments" ON payments; +DROP POLICY IF EXISTS "System can create payments" ON payments; + +CREATE POLICY "Everyone can read payments" ON payments FOR SELECT USING (true); +CREATE POLICY "Anyone can update payments" ON payments FOR UPDATE USING (true); +CREATE POLICY "System can create payments" ON payments FOR INSERT WITH CHECK (true); + +-- 5. Create chat_messages table +CREATE TABLE IF NOT EXISTS chat_messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + content_text TEXT, + content_image_urls TEXT[], + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + reactions JSONB DEFAULT '{}'::jsonb +); + +CREATE INDEX IF NOT EXISTS chat_messages_created_at_idx ON chat_messages(created_at); +ALTER TABLE chat_messages ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Everyone can read messages" ON chat_messages; +DROP POLICY IF EXISTS "Users can create own messages" ON chat_messages; +DROP POLICY IF EXISTS "Users can delete own messages" ON chat_messages; + +CREATE POLICY "Everyone can read messages" ON chat_messages FOR SELECT USING (true); +CREATE POLICY "Users can create own messages" ON chat_messages FOR INSERT WITH CHECK (true); +CREATE POLICY "Users can delete own messages" ON chat_messages FOR DELETE USING (true); + +-- 6. Create moments table +CREATE TABLE IF NOT EXISTS moments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + image_url TEXT NOT NULL, + caption TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS moments_user_id_idx ON moments(user_id); +CREATE INDEX IF NOT EXISTS moments_created_at_idx ON moments(created_at DESC); +ALTER TABLE moments ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Everyone can read moments" ON moments; +DROP POLICY IF EXISTS "Users can create own moments" ON moments; +DROP POLICY IF EXISTS "Users can delete own moments" ON moments; + +CREATE POLICY "Everyone can read moments" ON moments FOR SELECT USING (true); +CREATE POLICY "Users can create own moments" ON moments FOR INSERT WITH CHECK (true); +CREATE POLICY "Users can delete own moments" ON moments FOR DELETE USING (true); + +-- 7. Create transactions table (NEW!) +CREATE TABLE IF NOT EXISTS transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + spot_id UUID NOT NULL REFERENCES spots(id) ON DELETE CASCADE, + amount NUMERIC NOT NULL, + payment_method TEXT NOT NULL DEFAULT 'UPI', + status TEXT NOT NULL DEFAULT 'not_paid' CHECK (status IN ('paid', 'not_paid')), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS transactions_user_id_idx ON transactions(user_id); +CREATE INDEX IF NOT EXISTS transactions_spot_id_idx ON transactions(spot_id); +CREATE INDEX IF NOT EXISTS transactions_created_at_idx ON transactions(created_at DESC); +ALTER TABLE transactions ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Users can read own transactions" ON transactions; +DROP POLICY IF EXISTS "Admins can read all transactions" ON transactions; +DROP POLICY IF EXISTS "Admins can create transactions" ON transactions; +DROP POLICY IF EXISTS "Admins can update transactions" ON transactions; + +CREATE POLICY "Users can read own transactions" ON transactions FOR SELECT USING (true); +CREATE POLICY "Admins can read all transactions" ON transactions FOR SELECT USING (true); +CREATE POLICY "Admins can create transactions" ON transactions FOR INSERT WITH CHECK (true); +CREATE POLICY "Admins can update transactions" ON transactions FOR UPDATE USING (true); + +-- 8. Create trigger for transactions updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS update_transactions_updated_at ON transactions; +CREATE TRIGGER update_transactions_updated_at + BEFORE UPDATE ON transactions + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 9. Insert initial admin user +INSERT INTO profiles (id, name, username, email, phone, password, role, location, is_verified) +VALUES ( + '00000000-0000-0000-0000-000000000001', + 'Ram', + 'ramvj2005', + 'ramvj2005@gmail.com', + '7826821130', + 'ramkumar', + 'admin', + 'Attibele', + true +) +ON CONFLICT (id) DO NOTHING; + +-- 10. Insert other users +INSERT INTO profiles (id, name, username, phone, role, location, is_verified) +VALUES + ('00000000-0000-0000-0000-000000000002', 'Dhanush', 'dhanush', '9994323520', 'user', 'Attibele', true), + ('00000000-0000-0000-0000-000000000003', 'Godwin', 'godwin', '8903955341', 'user', 'Attibele', true), + ('00000000-0000-0000-0000-000000000004', 'Tharun', 'tharun', '9345624112', 'user', 'Attibele', true), + ('00000000-0000-0000-0000-000000000005', 'Sanjay', 'sanjay', '9865703667', 'user', 'Attibele', true), + ('00000000-0000-0000-0000-000000000006', 'Soundar', 'soundar', '9566686921', 'user', 'Attibele', true), + ('00000000-0000-0000-0000-000000000007', 'Jagadeesh', 'jagadeesh', '6381038172', 'user', 'Attibele', true), + ('00000000-0000-0000-0000-000000000008', 'Ram', 'ram', '7826821130', 'user', 'Attibele', true), + ('00000000-0000-0000-0000-000000000009', 'Lingesh', 'lingesh', '', 'user', 'Attibele', true) +ON CONFLICT (id) DO NOTHING; + +-- Success message +SELECT 'All tables created successfully including transactions!' as status; diff --git a/supabase_migration_transactions.sql b/supabase_migration_transactions.sql new file mode 100644 index 0000000..03f4570 --- /dev/null +++ b/supabase_migration_transactions.sql @@ -0,0 +1,74 @@ +-- Migration: Add transactions table for payment history tracking +-- This table stores transaction records for all payments made by users + +-- Create transactions table +CREATE TABLE IF NOT EXISTS transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + spot_id UUID NOT NULL REFERENCES spots(id) ON DELETE CASCADE, + amount NUMERIC NOT NULL, + payment_method TEXT NOT NULL DEFAULT 'UPI', + status TEXT NOT NULL DEFAULT 'not_paid' CHECK (status IN ('paid', 'not_paid')), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create indexes for faster queries +CREATE INDEX IF NOT EXISTS transactions_user_id_idx ON transactions(user_id); +CREATE INDEX IF NOT EXISTS transactions_spot_id_idx ON transactions(spot_id); +CREATE INDEX IF NOT EXISTS transactions_created_at_idx ON transactions(created_at DESC); + +-- Enable Row Level Security (RLS) +ALTER TABLE transactions ENABLE ROW LEVEL SECURITY; + +-- Policy: Users can read their own transactions +CREATE POLICY "Users can read own transactions" ON transactions + FOR SELECT USING (auth.uid() = user_id); + +-- Policy: Admins can read all transactions +CREATE POLICY "Admins can read all transactions" ON transactions + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM profiles + WHERE profiles.id = auth.uid() + AND profiles.role = 'admin' + ) + ); + +-- Policy: Admins can create transactions +CREATE POLICY "Admins can create transactions" ON transactions + FOR INSERT WITH CHECK ( + EXISTS ( + SELECT 1 FROM profiles + WHERE profiles.id = auth.uid() + AND profiles.role = 'admin' + ) + ); + +-- Policy: Admins can update transactions +CREATE POLICY "Admins can update transactions" ON transactions + FOR UPDATE USING ( + EXISTS ( + SELECT 1 FROM profiles + WHERE profiles.id = auth.uid() + AND profiles.role = 'admin' + ) + ); + +-- Add trigger to update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_transactions_updated_at + BEFORE UPDATE ON transactions + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Enable real-time for transactions table +-- Note: You need to enable this in Supabase Dashboard > Database > Replication +-- or run: ALTER PUBLICATION supabase_realtime ADD TABLE transactions; diff --git a/types.ts b/types.ts index e17ce54..76fe1bc 100644 --- a/types.ts +++ b/types.ts @@ -43,7 +43,11 @@ export interface Spot { day: string; timing: string; budget: number; +<<<<<<< HEAD + location: string; +======= location:string; +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b created_by: string; // User ID feedback?: string; description?: string; @@ -103,6 +107,21 @@ export interface Payment { drink_total_amount?: number; // Total amount for selected drinks } +<<<<<<< HEAD +export interface Transaction { + id: string; // UUID + user_id: string; // UUID + spot_id: string; // UUID + amount: number; + payment_method: string; + status: PaymentStatus; + created_at: string; // ISO String + profiles?: UserProfile; // Joined data from profiles table + spots?: Spot; // Joined data from spots table +} + +======= +>>>>>>> bedb01a0af53821680ce26a67bce5af226a10c8b export interface DrinkBrand { id: string; // UUID name: string; diff --git a/vite.config.ts b/vite.config.ts index ee5fb8d..209fba0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,21 +3,31 @@ import { defineConfig, loadEnv } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig(({ mode }) => { - const env = loadEnv(mode, '.', ''); - return { - server: { - port: 3000, - host: '0.0.0.0', - }, - plugins: [react()], - define: { - 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), - 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) - }, - resolve: { - alias: { - '@': path.resolve(__dirname, '.'), + const env = loadEnv(mode, '.', ''); + return { + root: '.', + publicDir: 'public', + server: { + port: 3000, + host: '0.0.0.0', + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + }, + build: { + outDir: 'dist', + rollupOptions: { + input: { + main: path.resolve(__dirname, 'index.html') } } - }; + } + }; });