mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
Merge pull request #1 from FranP-code/copilot/fix-c03dd869-9081-4f5e-a7cf-c61635f87fb6
This commit is contained in:
18
.env.example
18
.env.example
@@ -1,13 +1,19 @@
|
|||||||
# Environment variables for Inbox Negotiator
|
# Environment variables for Inbox Negotiator
|
||||||
|
|
||||||
# Supabase Configuration
|
# Appwrite Configuration
|
||||||
SUPABASE_URL=your_supabase_url_here
|
PUBLIC_APPWRITE_ENDPOINT=your_appwrite_endpoint_here
|
||||||
SUPABASE_ANON_KEY=your_supabase_anon_key_here
|
PUBLIC_APPWRITE_PROJECT_ID=your_appwrite_project_id_here
|
||||||
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here
|
PUBLIC_APPWRITE_DATABASE_ID=your_appwrite_database_id_here
|
||||||
|
APPWRITE_API_KEY=your_appwrite_api_key_here
|
||||||
|
|
||||||
|
# Legacy Supabase Configuration (for migration reference)
|
||||||
|
# SUPABASE_URL=your_supabase_url_here
|
||||||
|
# SUPABASE_ANON_KEY=your_supabase_anon_key_here
|
||||||
|
# SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here
|
||||||
|
|
||||||
# Google Generative AI API Key for Gemini model
|
# Google Generative AI API Key for Gemini model
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
||||||
|
|
||||||
# Add these same variables to your actual .env file
|
# Add these same variables to your actual .env file
|
||||||
# Note: The SUPABASE_SERVICE_ROLE_KEY is required for webhook operations
|
# Note: The APPWRITE_API_KEY is required for server-side operations
|
||||||
# to bypass Row Level Security (RLS) policies in server-side contexts
|
# and webhook operations with admin privileges
|
||||||
|
|||||||
60
.github/workflows/deploy-appwrite-functions.yml
vendored
Normal file
60
.github/workflows/deploy-appwrite-functions.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Deploy Appwrite Functions
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'appwrite/functions/**'
|
||||||
|
- 'supabase/functions/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-functions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Appwrite CLI
|
||||||
|
run: npm install -g appwrite-cli
|
||||||
|
|
||||||
|
- name: Setup Appwrite CLI
|
||||||
|
run: |
|
||||||
|
appwrite client \
|
||||||
|
--endpoint ${{ secrets.APPWRITE_ENDPOINT }} \
|
||||||
|
--project-id ${{ secrets.APPWRITE_PROJECT_ID }} \
|
||||||
|
--key ${{ secrets.APPWRITE_API_KEY }}
|
||||||
|
|
||||||
|
- name: Deploy Functions
|
||||||
|
run: ./scripts/deploy-appwrite-functions.sh
|
||||||
|
env:
|
||||||
|
APPWRITE_PROJECT_ID: ${{ secrets.APPWRITE_PROJECT_ID }}
|
||||||
|
APPWRITE_API_KEY: ${{ secrets.APPWRITE_API_KEY }}
|
||||||
|
APPWRITE_ENDPOINT: ${{ secrets.APPWRITE_ENDPOINT }}
|
||||||
|
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
|
||||||
|
POSTMARK_SERVER_TOKEN: ${{ secrets.POSTMARK_SERVER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create deployment summary
|
||||||
|
run: |
|
||||||
|
echo "## 🚀 Function Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Deployed at: $(date)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Functions deployed:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
for func in appwrite/functions/*/; do
|
||||||
|
if [ -d "$func" ]; then
|
||||||
|
echo "- $(basename "$func")" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
done
|
||||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Use pnpm as the package manager for this project
|
||||||
|
# This ensures consistent dependency management across all environments
|
||||||
|
package-manager=pnpm
|
||||||
154
APPWRITE_MIGRATION.md
Normal file
154
APPWRITE_MIGRATION.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Appwrite Migration Progress
|
||||||
|
|
||||||
|
## Completed Migrations
|
||||||
|
|
||||||
|
### 1. Dependencies
|
||||||
|
- ✅ Installed Appwrite SDK v16.0.2
|
||||||
|
- ✅ Removed @supabase/supabase-js dependency
|
||||||
|
- ✅ Updated package.json
|
||||||
|
|
||||||
|
### 2. Core Configuration
|
||||||
|
- ✅ Created `src/lib/appwrite.ts` - Main client configuration
|
||||||
|
- ✅ Created `src/lib/appwrite-admin.ts` - Server-side admin operations
|
||||||
|
- ✅ Updated environment variables in `.env.example`
|
||||||
|
|
||||||
|
### 3. Authentication Components
|
||||||
|
- ✅ `src/components/AuthForm.tsx` - Migrated to Appwrite Account API
|
||||||
|
- ✅ `src/components/AuthGuard.tsx` - Updated session management
|
||||||
|
- ✅ `src/components/Navbar.tsx` - Updated user state handling
|
||||||
|
|
||||||
|
### 4. Database Operations
|
||||||
|
- ✅ `src/components/Dashboard.tsx` - Partially migrated database queries
|
||||||
|
- ✅ `src/pages/api/postmark.ts` - Migrated webhook API to use Appwrite
|
||||||
|
- ✅ `src/components/DebtCard.tsx` - Migrated debt operations and function calls
|
||||||
|
- 🔄 `src/components/Configuration.tsx` - Partially migrated user data fetching
|
||||||
|
|
||||||
|
### 5. Function Calls
|
||||||
|
- ✅ Updated function invocation from Supabase to Appwrite Functions API
|
||||||
|
- ✅ Changed authentication headers from Bearer tokens to X-Appwrite-Project/X-Appwrite-Key
|
||||||
|
|
||||||
|
## Required Appwrite Setup
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
||||||
|
PUBLIC_APPWRITE_PROJECT_ID=your_project_id
|
||||||
|
PUBLIC_APPWRITE_DATABASE_ID=your_database_id
|
||||||
|
APPWRITE_API_KEY=your_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Deploy Functions Setup
|
||||||
|
|
||||||
|
#### 1. Install Appwrite CLI
|
||||||
|
```bash
|
||||||
|
npm install -g appwrite-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Initialize Appwrite Project
|
||||||
|
```bash
|
||||||
|
pnpm run setup:appwrite
|
||||||
|
# or manually: appwrite init project --project-id your_project_id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Deploy Functions Automatically
|
||||||
|
```bash
|
||||||
|
# Deploy all functions to Appwrite
|
||||||
|
pnpm run deploy:functions
|
||||||
|
|
||||||
|
# Or run the script directly
|
||||||
|
./scripts/deploy-appwrite-functions.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. GitHub Actions Auto-Deploy
|
||||||
|
The repository includes a GitHub Actions workflow (`.github/workflows/deploy-appwrite-functions.yml`) that automatically deploys functions when:
|
||||||
|
- Changes are pushed to the `main` branch
|
||||||
|
- Files in `appwrite/functions/` or `supabase/functions/` are modified
|
||||||
|
- Manually triggered via workflow dispatch
|
||||||
|
|
||||||
|
**Required GitHub Secrets:**
|
||||||
|
- `APPWRITE_PROJECT_ID` - Your Appwrite project ID
|
||||||
|
- `APPWRITE_API_KEY` - Your Appwrite API key with Functions write permissions
|
||||||
|
- `APPWRITE_ENDPOINT` - Your Appwrite endpoint (default: https://cloud.appwrite.io/v1)
|
||||||
|
- `GOOGLE_AI_API_KEY` - For AI-powered functions
|
||||||
|
- `POSTMARK_SERVER_TOKEN` - For email functions
|
||||||
|
|
||||||
|
### Database Collections to Create
|
||||||
|
1. **debts** - Main debt records
|
||||||
|
2. **audit_logs** - Action logging
|
||||||
|
3. **user_profiles** - User settings and preferences
|
||||||
|
4. **additional_emails** - Secondary email addresses
|
||||||
|
5. **email_processing_usage** - Monthly usage tracking
|
||||||
|
6. **debt_variables** - Custom debt variables
|
||||||
|
7. **conversation_messages** - Email conversation history
|
||||||
|
8. **users** - User personal data
|
||||||
|
|
||||||
|
### Functions to Migrate
|
||||||
|
1. **negotiate** - AI debt negotiation logic
|
||||||
|
2. **approve-debt** - Debt approval workflow
|
||||||
|
3. **send-email** - Email sending functionality
|
||||||
|
4. **analyze-response** - Email response analysis
|
||||||
|
5. **test-extraction** - Debt information extraction testing
|
||||||
|
|
||||||
|
## Remaining Tasks
|
||||||
|
|
||||||
|
### Components Not Yet Migrated
|
||||||
|
- `src/components/ConversationTimeline.tsx`
|
||||||
|
- `src/components/OnboardingDialog.tsx`
|
||||||
|
- `src/components/ManualResponseDialog.tsx`
|
||||||
|
- `src/components/DebtTimeline.tsx`
|
||||||
|
- `src/components/RealtimeTestButton.tsx`
|
||||||
|
- `src/components/ExtractionTester.tsx`
|
||||||
|
|
||||||
|
### Migration Notes
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
- Replaced `supabase.auth.signUp()` with `account.create()` + `account.createEmailPasswordSession()`
|
||||||
|
- Replaced `supabase.auth.signInWithPassword()` with `account.createEmailPasswordSession()`
|
||||||
|
- Replaced `supabase.auth.getUser()` with `account.get()`
|
||||||
|
- Replaced `supabase.auth.signOut()` with `account.deleteSession('current')`
|
||||||
|
|
||||||
|
#### Database Operations
|
||||||
|
- Replaced `supabase.from().select()` with `databases.listDocuments()`
|
||||||
|
- Replaced `supabase.from().insert()` with `databases.createDocument()`
|
||||||
|
- Replaced `supabase.from().update()` with `databases.updateDocument()`
|
||||||
|
- Note: Appwrite queries use different syntax than Supabase filters
|
||||||
|
|
||||||
|
#### Function Calls
|
||||||
|
- Replaced `supabase.functions.invoke()` with `functions.createExecution()`
|
||||||
|
- Changed authentication from Authorization header to X-Appwrite-Project/X-Appwrite-Key headers
|
||||||
|
|
||||||
|
#### Real-time Updates
|
||||||
|
- Supabase real-time subscriptions need to be replaced with Appwrite real-time
|
||||||
|
- Currently using polling as a temporary fallback
|
||||||
|
|
||||||
|
## Testing Required
|
||||||
|
|
||||||
|
1. **Authentication Flow**
|
||||||
|
- User registration
|
||||||
|
- User login/logout
|
||||||
|
- Session persistence
|
||||||
|
|
||||||
|
2. **Database Operations**
|
||||||
|
- Debt creation and updates
|
||||||
|
- User profile management
|
||||||
|
- Audit logging
|
||||||
|
|
||||||
|
3. **Function Execution**
|
||||||
|
- Email sending
|
||||||
|
- AI negotiation
|
||||||
|
- Response analysis
|
||||||
|
|
||||||
|
4. **API Endpoints**
|
||||||
|
- Postmark webhook processing
|
||||||
|
- Email parsing and storage
|
||||||
|
|
||||||
|
## Production Deployment Checklist
|
||||||
|
|
||||||
|
1. Set up Appwrite project and database
|
||||||
|
2. Create all required collections with proper schemas
|
||||||
|
3. Deploy Appwrite Functions (migrated from Supabase Edge Functions)
|
||||||
|
4. Configure environment variables
|
||||||
|
5. Set up proper permissions and security rules
|
||||||
|
6. Test all functionality end-to-end
|
||||||
|
7. Migrate data from Supabase to Appwrite
|
||||||
|
8. Update DNS/domain configuration if needed
|
||||||
71
README.md
71
README.md
@@ -7,17 +7,48 @@ An AI-powered system that automatically negotiates debt collections and billing
|
|||||||
- **AI Email Processing**: Automatically parses incoming emails to extract debt information using Google's Gemini AI
|
- **AI Email Processing**: Automatically parses incoming emails to extract debt information using Google's Gemini AI
|
||||||
- **Automated Negotiation**: Triggers negotiation workflows for legitimate debt collection notices
|
- **Automated Negotiation**: Triggers negotiation workflows for legitimate debt collection notices
|
||||||
- **Webhook Integration**: Seamlessly processes emails through Postmark webhook integration
|
- **Webhook Integration**: Seamlessly processes emails through Postmark webhook integration
|
||||||
- **Row Level Security**: Secure database operations with proper authentication handling
|
- **Secure Database Operations**: Uses Appwrite's document-level permissions for secure data access
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
This project uses **pnpm** as the package manager. Make sure you have pnpm installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g pnpm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone <repository-url>
|
||||||
|
cd inbox-negotiator
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Scripts
|
||||||
|
|
||||||
|
- `pnpm run dev` - Start development server
|
||||||
|
- `pnpm run build` - Build for production
|
||||||
|
- `pnpm run preview` - Preview production build
|
||||||
|
- `pnpm run deploy:functions` - Deploy Appwrite functions
|
||||||
|
- `pnpm run setup:appwrite` - Initialize Appwrite project
|
||||||
|
|
||||||
## Environment Setup
|
## Environment Setup
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and configure the following variables:
|
Copy `.env.example` to `.env` and configure the following variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Supabase Configuration
|
# Appwrite Configuration
|
||||||
SUPABASE_URL=your_supabase_url_here
|
PUBLIC_APPWRITE_ENDPOINT=your_appwrite_endpoint_here
|
||||||
SUPABASE_ANON_KEY=your_supabase_anon_key_here
|
PUBLIC_APPWRITE_PROJECT_ID=your_appwrite_project_id_here
|
||||||
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here
|
PUBLIC_APPWRITE_DATABASE_ID=your_appwrite_database_id_here
|
||||||
|
APPWRITE_API_KEY=your_appwrite_api_key_here
|
||||||
|
|
||||||
# Google Generative AI API Key for Gemini model
|
# Google Generative AI API Key for Gemini model
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
||||||
@@ -25,11 +56,23 @@ GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
|||||||
|
|
||||||
### Required Environment Variables
|
### Required Environment Variables
|
||||||
|
|
||||||
- `SUPABASE_URL`: Your Supabase project URL
|
- `PUBLIC_APPWRITE_ENDPOINT`: Your Appwrite instance endpoint (e.g., https://cloud.appwrite.io/v1)
|
||||||
- `SUPABASE_ANON_KEY`: Supabase anonymous key for client-side operations
|
- `PUBLIC_APPWRITE_PROJECT_ID`: Appwrite project ID
|
||||||
- `SUPABASE_SERVICE_ROLE_KEY`: Supabase service role key for server-side operations (bypasses RLS)
|
- `PUBLIC_APPWRITE_DATABASE_ID`: Appwrite database ID for the application
|
||||||
|
- `APPWRITE_API_KEY`: Appwrite API key for server-side operations (webhooks, functions)
|
||||||
- `GOOGLE_GENERATIVE_AI_API_KEY`: Google API key for AI processing
|
- `GOOGLE_GENERATIVE_AI_API_KEY`: Google API key for AI processing
|
||||||
|
|
||||||
|
## Migration from Supabase
|
||||||
|
|
||||||
|
This application has been migrated from Supabase to Appwrite. Key changes include:
|
||||||
|
|
||||||
|
- **Authentication**: Migrated from Supabase Auth to Appwrite Account API
|
||||||
|
- **Database**: Moved from Supabase tables to Appwrite collections
|
||||||
|
- **Functions**: Migrated from Supabase Edge Functions to Appwrite Functions
|
||||||
|
- **Real-time**: Updated from Supabase channels to Appwrite real-time subscriptions
|
||||||
|
|
||||||
|
For detailed migration notes, see [APPWRITE_MIGRATION.md](./APPWRITE_MIGRATION.md).
|
||||||
|
|
||||||
## Webhook Configuration
|
## Webhook Configuration
|
||||||
|
|
||||||
The `/api/postmark` endpoint handles incoming email webhooks from Postmark. It:
|
The `/api/postmark` endpoint handles incoming email webhooks from Postmark. It:
|
||||||
@@ -37,23 +80,23 @@ The `/api/postmark` endpoint handles incoming email webhooks from Postmark. It:
|
|||||||
1. Validates incoming email data
|
1. Validates incoming email data
|
||||||
2. Processes opt-out requests
|
2. Processes opt-out requests
|
||||||
3. Uses AI to extract debt information
|
3. Uses AI to extract debt information
|
||||||
4. Stores processed data in Supabase
|
4. Stores processed data in Appwrite
|
||||||
5. Triggers automated negotiation workflows
|
5. Triggers automated negotiation workflows
|
||||||
|
|
||||||
### RLS (Row Level Security) Handling
|
### Security Handling
|
||||||
|
|
||||||
The webhook uses a service role client to bypass RLS policies, ensuring server-side operations can write to the database without user authentication. This is essential for webhook operations where no user session exists.
|
The webhook uses an Appwrite admin client with API key authentication, ensuring server-side operations can write to the database without user authentication. This is essential for webhook operations where no user session exists.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
pnpm install
|
npm install
|
||||||
|
|
||||||
# Start development server
|
# Start development server
|
||||||
pnpm dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Ensure all environment variables are configured in your deployment environment, especially the `SUPABASE_SERVICE_ROLE_KEY` which is critical for webhook operations.
|
Ensure all environment variables are configured in your deployment environment, especially the `APPWRITE_API_KEY` which is critical for webhook operations.
|
||||||
|
|||||||
85
appwrite/appwrite.json
Normal file
85
appwrite/appwrite.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"projectId": "$APPWRITE_PROJECT_ID",
|
||||||
|
"projectName": "InboxNegotiator",
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "negotiate",
|
||||||
|
"functionId": "negotiate",
|
||||||
|
"runtime": "node-18.0",
|
||||||
|
"entrypoint": "src/main.js",
|
||||||
|
"commands": "npm install",
|
||||||
|
"timeout": 15,
|
||||||
|
"enabled": true,
|
||||||
|
"execute": ["any"],
|
||||||
|
"events": [],
|
||||||
|
"schedule": "",
|
||||||
|
"env": {
|
||||||
|
"GOOGLE_AI_API_KEY": "$GOOGLE_AI_API_KEY",
|
||||||
|
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "approve-debt",
|
||||||
|
"functionId": "approve-debt",
|
||||||
|
"runtime": "node-18.0",
|
||||||
|
"entrypoint": "src/main.js",
|
||||||
|
"commands": "npm install",
|
||||||
|
"timeout": 15,
|
||||||
|
"enabled": true,
|
||||||
|
"execute": ["any"],
|
||||||
|
"events": [],
|
||||||
|
"schedule": "",
|
||||||
|
"env": {
|
||||||
|
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "send-email",
|
||||||
|
"functionId": "send-email",
|
||||||
|
"runtime": "node-18.0",
|
||||||
|
"entrypoint": "src/main.js",
|
||||||
|
"commands": "npm install",
|
||||||
|
"timeout": 15,
|
||||||
|
"enabled": true,
|
||||||
|
"execute": ["any"],
|
||||||
|
"events": [],
|
||||||
|
"schedule": "",
|
||||||
|
"env": {
|
||||||
|
"POSTMARK_SERVER_TOKEN": "$POSTMARK_SERVER_TOKEN",
|
||||||
|
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "analyze-response",
|
||||||
|
"functionId": "analyze-response",
|
||||||
|
"runtime": "node-18.0",
|
||||||
|
"entrypoint": "src/main.js",
|
||||||
|
"commands": "npm install",
|
||||||
|
"timeout": 15,
|
||||||
|
"enabled": true,
|
||||||
|
"execute": ["any"],
|
||||||
|
"events": [],
|
||||||
|
"schedule": "",
|
||||||
|
"env": {
|
||||||
|
"GOOGLE_AI_API_KEY": "$GOOGLE_AI_API_KEY",
|
||||||
|
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test-extraction",
|
||||||
|
"functionId": "test-extraction",
|
||||||
|
"runtime": "node-18.0",
|
||||||
|
"entrypoint": "src/main.js",
|
||||||
|
"commands": "npm install",
|
||||||
|
"timeout": 15,
|
||||||
|
"enabled": true,
|
||||||
|
"execute": ["any"],
|
||||||
|
"events": [],
|
||||||
|
"schedule": "",
|
||||||
|
"env": {
|
||||||
|
"GOOGLE_AI_API_KEY": "$GOOGLE_AI_API_KEY",
|
||||||
|
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro",
|
||||||
|
"deploy:functions": "./scripts/deploy-appwrite-functions.sh",
|
||||||
|
"setup:appwrite": "appwrite init project"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/google": "^1.2.19",
|
"@ai-sdk/google": "^1.2.19",
|
||||||
@@ -44,11 +46,11 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@supabase/supabase-js": "^2.50.0",
|
|
||||||
"@types/react": "^18.3.10",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.5.0",
|
||||||
"ai": "^4.3.16",
|
"ai": "^4.3.16",
|
||||||
|
"appwrite": "^18.2.0",
|
||||||
"astro": "^5.9.0",
|
"astro": "^5.9.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
101
pnpm-lock.yaml
generated
101
pnpm-lock.yaml
generated
@@ -107,9 +107,6 @@ importers:
|
|||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@supabase/supabase-js':
|
|
||||||
specifier: ^2.50.0
|
|
||||||
version: 2.50.0
|
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.3.10
|
specifier: ^18.3.10
|
||||||
version: 18.3.23
|
version: 18.3.23
|
||||||
@@ -122,6 +119,9 @@ importers:
|
|||||||
ai:
|
ai:
|
||||||
specifier: ^4.3.16
|
specifier: ^4.3.16
|
||||||
version: 4.3.16(react@18.3.1)(zod@3.23.8)
|
version: 4.3.16(react@18.3.1)(zod@3.23.8)
|
||||||
|
appwrite:
|
||||||
|
specifier: ^18.2.0
|
||||||
|
version: 18.2.0
|
||||||
astro:
|
astro:
|
||||||
specifier: ^5.9.0
|
specifier: ^5.9.0
|
||||||
version: 5.9.0(@types/node@22.15.30)(jiti@1.21.7)(rollup@4.42.0)(typescript@5.8.3)(yaml@2.8.0)
|
version: 5.9.0(@types/node@22.15.30)(jiti@1.21.7)(rollup@4.42.0)(typescript@5.8.3)(yaml@2.8.0)
|
||||||
@@ -1449,28 +1449,6 @@ packages:
|
|||||||
'@shikijs/vscode-textmate@10.0.2':
|
'@shikijs/vscode-textmate@10.0.2':
|
||||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||||
|
|
||||||
'@supabase/auth-js@2.70.0':
|
|
||||||
resolution: {integrity: sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==}
|
|
||||||
|
|
||||||
'@supabase/functions-js@2.4.4':
|
|
||||||
resolution: {integrity: sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==}
|
|
||||||
|
|
||||||
'@supabase/node-fetch@2.6.15':
|
|
||||||
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
|
|
||||||
engines: {node: 4.x || >=6.0.0}
|
|
||||||
|
|
||||||
'@supabase/postgrest-js@1.19.4':
|
|
||||||
resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==}
|
|
||||||
|
|
||||||
'@supabase/realtime-js@2.11.10':
|
|
||||||
resolution: {integrity: sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==}
|
|
||||||
|
|
||||||
'@supabase/storage-js@2.7.1':
|
|
||||||
resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==}
|
|
||||||
|
|
||||||
'@supabase/supabase-js@2.50.0':
|
|
||||||
resolution: {integrity: sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==}
|
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||||
|
|
||||||
@@ -1543,9 +1521,6 @@ packages:
|
|||||||
'@types/node@22.15.30':
|
'@types/node@22.15.30':
|
||||||
resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==}
|
resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==}
|
||||||
|
|
||||||
'@types/phoenix@1.6.6':
|
|
||||||
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
|
|
||||||
|
|
||||||
'@types/prop-types@15.7.14':
|
'@types/prop-types@15.7.14':
|
||||||
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
|
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
|
||||||
|
|
||||||
@@ -1560,9 +1535,6 @@ packages:
|
|||||||
'@types/unist@3.0.3':
|
'@types/unist@3.0.3':
|
||||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
|
||||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0':
|
'@ungap/structured-clone@1.3.0':
|
||||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||||
|
|
||||||
@@ -1666,6 +1638,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
appwrite@18.2.0:
|
||||||
|
resolution: {integrity: sha512-g7pQpsxqR7+amEIaQLXMN4XzdQKenTHnGdA4s7UUJdZufhlHdJby8895h8z893+S0XipeHZhi0wpxYA2An95Rg==}
|
||||||
|
|
||||||
arg@5.0.2:
|
arg@5.0.2:
|
||||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||||
|
|
||||||
@@ -3343,18 +3318,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==}
|
resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
|
|
||||||
ws@8.18.2:
|
|
||||||
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
bufferutil: ^4.0.1
|
|
||||||
utf-8-validate: '>=5.0.2'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
bufferutil:
|
|
||||||
optional: true
|
|
||||||
utf-8-validate:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
xxhash-wasm@1.1.0:
|
xxhash-wasm@1.1.0:
|
||||||
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
|
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
|
||||||
|
|
||||||
@@ -4686,48 +4649,6 @@ snapshots:
|
|||||||
|
|
||||||
'@shikijs/vscode-textmate@10.0.2': {}
|
'@shikijs/vscode-textmate@10.0.2': {}
|
||||||
|
|
||||||
'@supabase/auth-js@2.70.0':
|
|
||||||
dependencies:
|
|
||||||
'@supabase/node-fetch': 2.6.15
|
|
||||||
|
|
||||||
'@supabase/functions-js@2.4.4':
|
|
||||||
dependencies:
|
|
||||||
'@supabase/node-fetch': 2.6.15
|
|
||||||
|
|
||||||
'@supabase/node-fetch@2.6.15':
|
|
||||||
dependencies:
|
|
||||||
whatwg-url: 5.0.0
|
|
||||||
|
|
||||||
'@supabase/postgrest-js@1.19.4':
|
|
||||||
dependencies:
|
|
||||||
'@supabase/node-fetch': 2.6.15
|
|
||||||
|
|
||||||
'@supabase/realtime-js@2.11.10':
|
|
||||||
dependencies:
|
|
||||||
'@supabase/node-fetch': 2.6.15
|
|
||||||
'@types/phoenix': 1.6.6
|
|
||||||
'@types/ws': 8.18.1
|
|
||||||
ws: 8.18.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- utf-8-validate
|
|
||||||
|
|
||||||
'@supabase/storage-js@2.7.1':
|
|
||||||
dependencies:
|
|
||||||
'@supabase/node-fetch': 2.6.15
|
|
||||||
|
|
||||||
'@supabase/supabase-js@2.50.0':
|
|
||||||
dependencies:
|
|
||||||
'@supabase/auth-js': 2.70.0
|
|
||||||
'@supabase/functions-js': 2.4.4
|
|
||||||
'@supabase/node-fetch': 2.6.15
|
|
||||||
'@supabase/postgrest-js': 1.19.4
|
|
||||||
'@supabase/realtime-js': 2.11.10
|
|
||||||
'@supabase/storage-js': 2.7.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- utf-8-validate
|
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -4809,8 +4730,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/phoenix@1.6.6': {}
|
|
||||||
|
|
||||||
'@types/prop-types@15.7.14': {}
|
'@types/prop-types@15.7.14': {}
|
||||||
|
|
||||||
'@types/react-dom@18.3.7(@types/react@18.3.23)':
|
'@types/react-dom@18.3.7(@types/react@18.3.23)':
|
||||||
@@ -4824,10 +4743,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 22.15.30
|
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
|
|
||||||
'@vercel/analytics@1.5.0(react@18.3.1)':
|
'@vercel/analytics@1.5.0(react@18.3.1)':
|
||||||
@@ -4925,6 +4840,8 @@ snapshots:
|
|||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
appwrite@18.2.0: {}
|
||||||
|
|
||||||
arg@5.0.2: {}
|
arg@5.0.2: {}
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
@@ -6915,8 +6832,6 @@ snapshots:
|
|||||||
imurmurhash: 0.1.4
|
imurmurhash: 0.1.4
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
ws@8.18.2: {}
|
|
||||||
|
|
||||||
xxhash-wasm@1.1.0: {}
|
xxhash-wasm@1.1.0: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|||||||
222
scripts/deploy-appwrite-functions.sh
Executable file
222
scripts/deploy-appwrite-functions.sh
Executable file
@@ -0,0 +1,222 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Appwrite Functions Auto-Deploy Script
|
||||||
|
# This script migrates Supabase Edge Functions to Appwrite Functions
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SUPABASE_FUNCTIONS_DIR="./supabase/functions"
|
||||||
|
APPWRITE_FUNCTIONS_DIR="./appwrite/functions"
|
||||||
|
APPWRITE_CONFIG="./appwrite/appwrite.json"
|
||||||
|
|
||||||
|
echo -e "${GREEN}🚀 Appwrite Functions Auto-Deploy Script${NC}"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Check if Appwrite CLI is installed
|
||||||
|
if ! command -v appwrite &> /dev/null; then
|
||||||
|
echo -e "${RED}❌ Appwrite CLI not found${NC}"
|
||||||
|
echo "Please install it with: npm install -g appwrite-cli"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check environment variables
|
||||||
|
if [ -z "$APPWRITE_PROJECT_ID" ]; then
|
||||||
|
echo -e "${RED}❌ APPWRITE_PROJECT_ID environment variable not set${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$APPWRITE_API_KEY" ]; then
|
||||||
|
echo -e "${RED}❌ APPWRITE_API_KEY environment variable not set${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}📋 Environment Variables:${NC}"
|
||||||
|
echo "APPWRITE_PROJECT_ID: $APPWRITE_PROJECT_ID"
|
||||||
|
echo "APPWRITE_API_KEY: ${APPWRITE_API_KEY:0:8}..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Initialize Appwrite project if not already done
|
||||||
|
echo -e "${YELLOW}🔧 Setting up Appwrite project...${NC}"
|
||||||
|
if [ ! -f ".appwrite/project.json" ]; then
|
||||||
|
appwrite init project --project-id "$APPWRITE_PROJECT_ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create Appwrite functions directory if it doesn't exist
|
||||||
|
mkdir -p "$APPWRITE_FUNCTIONS_DIR"
|
||||||
|
|
||||||
|
# Function to convert Supabase function to Appwrite function
|
||||||
|
convert_function() {
|
||||||
|
local func_name=$1
|
||||||
|
local supabase_func_dir="$SUPABASE_FUNCTIONS_DIR/$func_name"
|
||||||
|
local appwrite_func_dir="$APPWRITE_FUNCTIONS_DIR/$func_name"
|
||||||
|
|
||||||
|
if [ ! -d "$supabase_func_dir" ]; then
|
||||||
|
echo -e "${RED}❌ Supabase function '$func_name' not found${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}🔄 Converting function: $func_name${NC}"
|
||||||
|
|
||||||
|
# Create Appwrite function directory
|
||||||
|
mkdir -p "$appwrite_func_dir/src"
|
||||||
|
|
||||||
|
# Copy and convert the main file
|
||||||
|
if [ -f "$supabase_func_dir/index.ts" ]; then
|
||||||
|
# Convert Supabase function to Appwrite function format
|
||||||
|
cat > "$appwrite_func_dir/src/main.js" << 'EOF'
|
||||||
|
import { Client, Databases, Functions } from 'node-appwrite';
|
||||||
|
|
||||||
|
// This is a migrated function from Supabase to Appwrite
|
||||||
|
// Original Supabase function logic should be adapted here
|
||||||
|
|
||||||
|
export default async ({ req, res, log, error }) => {
|
||||||
|
const client = new Client()
|
||||||
|
.setEndpoint(process.env.APPWRITE_FUNCTION_ENDPOINT)
|
||||||
|
.setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
|
||||||
|
.setKey(process.env.APPWRITE_API_KEY);
|
||||||
|
|
||||||
|
const databases = new Databases(client);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: Migrate Supabase function logic here
|
||||||
|
// Original file: ${supabase_func_dir}/index.ts
|
||||||
|
|
||||||
|
log('Function executed successfully');
|
||||||
|
return res.json({ success: true, message: 'Function migrated from Supabase' });
|
||||||
|
} catch (err) {
|
||||||
|
error('Function execution failed: ' + err.message);
|
||||||
|
return res.json({ success: false, error: err.message }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create package.json for the function
|
||||||
|
cat > "$appwrite_func_dir/package.json" << EOF
|
||||||
|
{
|
||||||
|
"name": "$func_name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Migrated from Supabase Edge Function",
|
||||||
|
"main": "src/main.js",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"node-appwrite": "^13.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create README with migration notes
|
||||||
|
cat > "$appwrite_func_dir/README.md" << EOF
|
||||||
|
# $func_name Function
|
||||||
|
|
||||||
|
This function was migrated from Supabase Edge Functions to Appwrite Functions.
|
||||||
|
|
||||||
|
## Original Supabase Function
|
||||||
|
- Location: \`$supabase_func_dir/index.ts\`
|
||||||
|
- Migrated on: $(date)
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
- The function logic needs to be manually adapted from the original Supabase function
|
||||||
|
- Update environment variables and dependencies as needed
|
||||||
|
- Test thoroughly before deploying to production
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
\`\`\`bash
|
||||||
|
appwrite functions createDeployment --function-id=$func_name --activate=true
|
||||||
|
\`\`\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Function '$func_name' converted successfully${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ No index.ts found for function '$func_name'${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deploy function to Appwrite
|
||||||
|
deploy_function() {
|
||||||
|
local func_name=$1
|
||||||
|
local appwrite_func_dir="$APPWRITE_FUNCTIONS_DIR/$func_name"
|
||||||
|
|
||||||
|
if [ ! -d "$appwrite_func_dir" ]; then
|
||||||
|
echo -e "${RED}❌ Appwrite function '$func_name' not found${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}🚀 Deploying function: $func_name${NC}"
|
||||||
|
|
||||||
|
# Create function if it doesn't exist
|
||||||
|
appwrite functions create \
|
||||||
|
--function-id="$func_name" \
|
||||||
|
--name="$func_name" \
|
||||||
|
--runtime="node-18.0" \
|
||||||
|
--execute='["any"]' \
|
||||||
|
--timeout=15 \
|
||||||
|
--enabled=true || echo "Function may already exist, continuing..."
|
||||||
|
|
||||||
|
# Deploy the function
|
||||||
|
cd "$appwrite_func_dir"
|
||||||
|
appwrite functions createDeployment \
|
||||||
|
--function-id="$func_name" \
|
||||||
|
--activate=true \
|
||||||
|
--code="."
|
||||||
|
cd - > /dev/null
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Function '$func_name' deployed successfully${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
echo -e "${YELLOW}📋 Available Supabase functions:${NC}"
|
||||||
|
for func_dir in "$SUPABASE_FUNCTIONS_DIR"/*; do
|
||||||
|
if [ -d "$func_dir" ]; then
|
||||||
|
func_name=$(basename "$func_dir")
|
||||||
|
echo " - $func_name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Convert functions
|
||||||
|
echo -e "${YELLOW}🔄 Converting Supabase functions to Appwrite format...${NC}"
|
||||||
|
for func_dir in "$SUPABASE_FUNCTIONS_DIR"/*; do
|
||||||
|
if [ -d "$func_dir" ]; then
|
||||||
|
func_name=$(basename "$func_dir")
|
||||||
|
convert_function "$func_name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}🚀 Deploying functions to Appwrite...${NC}"
|
||||||
|
|
||||||
|
# Ask for confirmation
|
||||||
|
read -p "Do you want to deploy all functions to Appwrite? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
for func_dir in "$APPWRITE_FUNCTIONS_DIR"/*; do
|
||||||
|
if [ -d "$func_dir" ]; then
|
||||||
|
func_name=$(basename "$func_dir")
|
||||||
|
deploy_function "$func_name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}🎉 All functions deployed successfully!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📝 Next steps:${NC}"
|
||||||
|
echo "1. Update each function's logic in appwrite/functions/*/src/main.js"
|
||||||
|
echo "2. Test functions in Appwrite console"
|
||||||
|
echo "3. Update your application to use Appwrite function IDs"
|
||||||
|
echo "4. Set up environment variables for each function"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⏭️ Function deployment skipped${NC}"
|
||||||
|
echo "You can deploy individual functions later using:"
|
||||||
|
echo " appwrite functions createDeployment --function-id=FUNCTION_NAME --activate=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✨ Migration process completed!${NC}"
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { supabase } from '../lib/supabase';
|
import { account } from '../lib/appwrite';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Loader2, Mail, Lock, User } from 'lucide-react';
|
import { Loader2, Mail, Lock, User } from 'lucide-react';
|
||||||
|
import { ID } from 'appwrite';
|
||||||
|
|
||||||
interface AuthFormProps {
|
interface AuthFormProps {
|
||||||
mode: 'login' | 'signup';
|
mode: 'login' | 'signup';
|
||||||
@@ -27,32 +28,28 @@ export function AuthForm({ mode }: AuthFormProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (mode === 'signup') {
|
if (mode === 'signup') {
|
||||||
const { error } = await supabase.auth.signUp({
|
// Create account with Appwrite
|
||||||
|
await account.create(
|
||||||
|
ID.unique(),
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
options: {
|
fullName
|
||||||
data: {
|
);
|
||||||
full_name: fullName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
// Create session after account creation
|
||||||
setMessage('Check your email for the confirmation link!');
|
await account.createEmailPasswordSession(email, password);
|
||||||
|
|
||||||
|
setMessage('Account created successfully!');
|
||||||
window.location.href = '/dashboard';
|
window.location.href = '/dashboard';
|
||||||
} else {
|
} else {
|
||||||
const { error } = await supabase.auth.signInWithPassword({
|
// Sign in with Appwrite
|
||||||
email,
|
await account.createEmailPasswordSession(email, password);
|
||||||
password,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
// Redirect to dashboard on successful login
|
// Redirect to dashboard on successful login
|
||||||
window.location.href = '/dashboard';
|
window.location.href = '/dashboard';
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message);
|
setError(error.message || 'An error occurred');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { supabase } from '../lib/supabase';
|
import { account } from '../lib/appwrite';
|
||||||
import type { User } from '@supabase/supabase-js';
|
import type { Models } from 'appwrite';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
interface AuthGuardProps {
|
interface AuthGuardProps {
|
||||||
@@ -9,47 +9,38 @@ interface AuthGuardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AuthGuard({ children, requireAuth = true }: AuthGuardProps) {
|
export function AuthGuard({ children, requireAuth = true }: AuthGuardProps) {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Get initial session
|
// Get initial session
|
||||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
account.get().then((currentUser) => {
|
||||||
setUser(session?.user ?? null);
|
setUser(currentUser);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// Redirect logic
|
// Redirect logic
|
||||||
if (requireAuth && !session?.user) {
|
if (requireAuth && !currentUser) {
|
||||||
// User needs to be authenticated but isn't - redirect to login
|
// User needs to be authenticated but isn't - redirect to login
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
} else if (!requireAuth && session?.user) {
|
} else if (!requireAuth && currentUser) {
|
||||||
// User is authenticated but on a public page - redirect to dashboard
|
// User is authenticated but on a public page - redirect to dashboard
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
if (currentPath === '/login' || currentPath === '/signup') {
|
if (currentPath === '/login' || currentPath === '/signup') {
|
||||||
window.location.href = '/dashboard';
|
window.location.href = '/dashboard';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// No user session found
|
||||||
|
setUser(null);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (requireAuth) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for auth changes
|
// Note: Appwrite doesn't have built-in session listeners like Supabase
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
// You might need to implement session checking through other means or use Appwrite's real-time features
|
||||||
(event, session) => {
|
|
||||||
setUser(session?.user ?? null);
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
// Handle auth state changes
|
|
||||||
if (requireAuth && !session?.user) {
|
|
||||||
window.location.href = '/login';
|
|
||||||
} else if (!requireAuth && session?.user) {
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
if (currentPath === '/login' || currentPath === '/signup') {
|
|
||||||
window.location.href = '/dashboard';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => subscription.unsubscribe();
|
|
||||||
}, [requireAuth]);
|
}, [requireAuth]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
supabase,
|
account,
|
||||||
|
databases,
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS,
|
||||||
type AdditionalEmail,
|
type AdditionalEmail,
|
||||||
type UserProfile,
|
type UserProfile,
|
||||||
type EmailProcessingUsage,
|
type EmailProcessingUsage,
|
||||||
} from "../lib/supabase";
|
} from "../lib/appwrite";
|
||||||
|
import { ID, Query } from "appwrite";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
@@ -65,44 +69,45 @@ export function Configuration() {
|
|||||||
|
|
||||||
const fetchUserData = async () => {
|
const fetchUserData = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
// Fetch user profile
|
// Fetch user profile
|
||||||
const { data: profileData } = await supabase
|
const profileResponse = await databases.listDocuments(
|
||||||
.from("user_profiles")
|
DATABASE_ID,
|
||||||
.select("*")
|
COLLECTIONS.USER_PROFILES,
|
||||||
.eq("user_id", user.id)
|
[Query.equal('user_id', user.$id)]
|
||||||
.single();
|
);
|
||||||
|
const profileData = profileResponse.documents[0];
|
||||||
|
|
||||||
// Fetch user personal data
|
// Fetch user personal data from users collection
|
||||||
const { data: userData } = await supabase
|
const usersResponse = await databases.listDocuments(
|
||||||
.from("users")
|
DATABASE_ID,
|
||||||
.select("*")
|
'users', // Assuming users collection exists
|
||||||
.eq("id", user.id)
|
[Query.equal('id', user.$id)]
|
||||||
.single();
|
);
|
||||||
|
const userData = usersResponse.documents[0];
|
||||||
|
|
||||||
// Fetch additional emails
|
// Fetch additional emails
|
||||||
const { data: emailsData } = await supabase
|
const emailsResponse = await databases.listDocuments(
|
||||||
.from("additional_emails")
|
DATABASE_ID,
|
||||||
.select("*")
|
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||||
.eq("user_id", user.id)
|
[Query.equal('user_id', user.$id), Query.orderDesc('created_at')]
|
||||||
.order("created_at", { ascending: false });
|
);
|
||||||
|
const emailsData = emailsResponse.documents;
|
||||||
|
|
||||||
// Fetch current month usage
|
// Fetch current month usage
|
||||||
const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM
|
const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM
|
||||||
const { data: usageData } = await supabase
|
const usageResponse = await databases.listDocuments(
|
||||||
.from("email_processing_usage")
|
DATABASE_ID,
|
||||||
.select("*")
|
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||||
.eq("user_id", user.id)
|
[Query.equal('user_id', user.$id), Query.equal('month_year', currentMonth)]
|
||||||
.eq("month_year", currentMonth)
|
);
|
||||||
.single();
|
const usageData = usageResponse.documents[0];
|
||||||
|
|
||||||
setProfile(profileData);
|
setProfile(profileData as UserProfile);
|
||||||
setAdditionalEmails(emailsData || []);
|
setAdditionalEmails(emailsData as AdditionalEmail[]);
|
||||||
setUsage(usageData);
|
setUsage(usageData as EmailProcessingUsage);
|
||||||
|
|
||||||
// Set personal data
|
// Set personal data
|
||||||
if (userData) {
|
if (userData) {
|
||||||
@@ -131,25 +136,46 @@ export function Configuration() {
|
|||||||
const savePersonalData = async () => {
|
const savePersonalData = async () => {
|
||||||
setSavingPersonalData(true);
|
setSavingPersonalData(true);
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const { error } = await supabase
|
// First, try to get the existing user document
|
||||||
.from("users")
|
const usersResponse = await databases.listDocuments(
|
||||||
.update({
|
DATABASE_ID,
|
||||||
full_name: personalData.full_name || null,
|
'users',
|
||||||
address_line_1: personalData.address_line_1 || null,
|
[Query.equal('id', user.$id)]
|
||||||
address_line_2: personalData.address_line_2 || null,
|
);
|
||||||
city: personalData.city || null,
|
|
||||||
state: personalData.state || null,
|
|
||||||
zip_code: personalData.zip_code || null,
|
|
||||||
phone_number: personalData.phone_number || null,
|
|
||||||
})
|
|
||||||
.eq("id", user.id);
|
|
||||||
|
|
||||||
if (error) throw error;
|
const updateData = {
|
||||||
|
full_name: personalData.full_name || null,
|
||||||
|
address_line_1: personalData.address_line_1 || null,
|
||||||
|
address_line_2: personalData.address_line_2 || null,
|
||||||
|
city: personalData.city || null,
|
||||||
|
state: personalData.state || null,
|
||||||
|
zip_code: personalData.zip_code || null,
|
||||||
|
phone_number: personalData.phone_number || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (usersResponse.documents.length > 0) {
|
||||||
|
// Update existing document
|
||||||
|
await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
'users',
|
||||||
|
usersResponse.documents[0].$id,
|
||||||
|
updateData
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Create new document if it doesn't exist
|
||||||
|
await databases.createDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
'users',
|
||||||
|
ID.unique(),
|
||||||
|
{
|
||||||
|
id: user.$id,
|
||||||
|
...updateData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
toast.success("Personal data updated", {
|
toast.success("Personal data updated", {
|
||||||
description: "Your personal information has been saved successfully.",
|
description: "Your personal information has been saved successfully.",
|
||||||
@@ -166,19 +192,42 @@ export function Configuration() {
|
|||||||
const saveServerToken = async () => {
|
const saveServerToken = async () => {
|
||||||
setSavingServerToken(true);
|
setSavingServerToken(true);
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const { error } = await supabase
|
// Get the existing user profile document
|
||||||
.from("user_profiles")
|
const profileResponse = await databases.listDocuments(
|
||||||
.update({
|
DATABASE_ID,
|
||||||
postmark_server_token: serverToken || null,
|
COLLECTIONS.USER_PROFILES,
|
||||||
})
|
[Query.equal('user_id', user.$id)]
|
||||||
.eq("user_id", user.id);
|
);
|
||||||
|
|
||||||
if (error) throw error;
|
if (profileResponse.documents.length > 0) {
|
||||||
|
// Update existing profile
|
||||||
|
await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.USER_PROFILES,
|
||||||
|
profileResponse.documents[0].$id,
|
||||||
|
{
|
||||||
|
postmark_server_token: serverToken || null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Create new profile if it doesn't exist
|
||||||
|
await databases.createDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.USER_PROFILES,
|
||||||
|
ID.unique(),
|
||||||
|
{
|
||||||
|
user_id: user.$id,
|
||||||
|
postmark_server_token: serverToken || null,
|
||||||
|
onboarding_completed: false,
|
||||||
|
email_processing_limit: 1000,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update local profile state
|
// Update local profile state
|
||||||
setProfile((prev) =>
|
setProfile((prev) =>
|
||||||
@@ -202,23 +251,24 @@ export function Configuration() {
|
|||||||
|
|
||||||
setAddingEmail(true);
|
setAddingEmail(true);
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const newEmailDoc = await databases.createDocument(
|
||||||
.from("additional_emails")
|
DATABASE_ID,
|
||||||
.insert({
|
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||||
user_id: user.id,
|
ID.unique(),
|
||||||
|
{
|
||||||
|
user_id: user.$id,
|
||||||
email_address: newEmail.trim().toLowerCase(),
|
email_address: newEmail.trim().toLowerCase(),
|
||||||
})
|
verified: false,
|
||||||
.select()
|
verification_token: null,
|
||||||
.single();
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (error) throw error;
|
setAdditionalEmails([newEmailDoc as AdditionalEmail, ...additionalEmails]);
|
||||||
|
|
||||||
setAdditionalEmails([data, ...additionalEmails]);
|
|
||||||
setNewEmail("");
|
setNewEmail("");
|
||||||
toast.success("Email added successfully", {
|
toast.success("Email added successfully", {
|
||||||
description: "Additional email has been added to your account.",
|
description: "Additional email has been added to your account.",
|
||||||
@@ -234,15 +284,14 @@ export function Configuration() {
|
|||||||
|
|
||||||
const removeAdditionalEmail = async (emailId: string) => {
|
const removeAdditionalEmail = async (emailId: string) => {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
await databases.deleteDocument(
|
||||||
.from("additional_emails")
|
DATABASE_ID,
|
||||||
.delete()
|
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||||
.eq("id", emailId);
|
emailId
|
||||||
|
);
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
setAdditionalEmails(
|
setAdditionalEmails(
|
||||||
additionalEmails.filter((email) => email.id !== emailId)
|
additionalEmails.filter((email) => email.$id !== emailId)
|
||||||
);
|
);
|
||||||
toast.success("Email removed", {
|
toast.success("Email removed", {
|
||||||
description: "Additional email has been removed from your account.",
|
description: "Additional email has been removed from your account.",
|
||||||
@@ -592,7 +641,7 @@ export function Configuration() {
|
|||||||
) : (
|
) : (
|
||||||
additionalEmails.map((email) => (
|
additionalEmails.map((email) => (
|
||||||
<div
|
<div
|
||||||
key={email.id}
|
key={email.$id}
|
||||||
className="flex items-center justify-between p-3 border rounded-lg"
|
className="flex items-center justify-between p-3 border rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -624,7 +673,7 @@ export function Configuration() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeAdditionalEmail(email.id)}
|
onClick={() => removeAdditionalEmail(email.$id)}
|
||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { supabase, type Debt, type UserProfile } from "../lib/supabase";
|
import { account, databases, DATABASE_ID, COLLECTIONS, type Debt, type UserProfile } from "../lib/appwrite";
|
||||||
|
import { Query } from "appwrite";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { DebtCard } from "./DebtCard";
|
import { DebtCard } from "./DebtCard";
|
||||||
import { ConversationTimeline } from "./ConversationTimeline";
|
// TODO: Migrate these components to Appwrite
|
||||||
import { OnboardingDialog } from "./OnboardingDialog";
|
// import { ConversationTimeline } from "./ConversationTimeline";
|
||||||
|
// import { OnboardingDialog } from "./OnboardingDialog";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
@@ -18,6 +20,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { formatCurrency } from "../lib/utils";
|
import { formatCurrency } from "../lib/utils";
|
||||||
|
import type { Models } from "appwrite";
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
const [debts, setDebts] = useState<Debt[]>([]);
|
const [debts, setDebts] = useState<Debt[]>([]);
|
||||||
@@ -43,18 +46,18 @@ export function Dashboard() {
|
|||||||
|
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const { data: profile } = await supabase
|
const response = await databases.listDocuments(
|
||||||
.from("user_profiles")
|
DATABASE_ID,
|
||||||
.select("*")
|
COLLECTIONS.USER_PROFILES,
|
||||||
.eq("user_id", user.id)
|
[Query.equal('user_id', user.$id)]
|
||||||
.single();
|
);
|
||||||
|
|
||||||
setUserProfile(profile);
|
// Get profile for current user
|
||||||
|
const profile = response.documents[0];
|
||||||
|
setUserProfile(profile as UserProfile);
|
||||||
|
|
||||||
// Show onboarding if user hasn't completed it
|
// Show onboarding if user hasn't completed it
|
||||||
if (profile && !profile.onboarding_completed) {
|
if (profile && !profile.onboarding_completed) {
|
||||||
@@ -67,19 +70,17 @@ export function Dashboard() {
|
|||||||
|
|
||||||
const fetchDebts = async () => {
|
const fetchDebts = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const response = await databases.listDocuments(
|
||||||
.from("debts")
|
DATABASE_ID,
|
||||||
.select("*")
|
COLLECTIONS.DEBTS,
|
||||||
.eq("user_id", user.id)
|
[Query.equal('user_id', user.$id), Query.orderDesc('created_at')]
|
||||||
.order("created_at", { ascending: false });
|
);
|
||||||
|
|
||||||
if (error) throw error;
|
// Debts are already filtered and sorted by the query
|
||||||
setDebts(data || []);
|
setDebts(response.documents as Debt[]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching debts:", error);
|
console.error("Error fetching debts:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -88,36 +89,24 @@ export function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setupRealtimeSubscription = () => {
|
const setupRealtimeSubscription = () => {
|
||||||
const subscription = supabase
|
// Appwrite real-time subscription for debts collection
|
||||||
.channel("debts_changes")
|
// Note: This is a simplified version. In production, you'd need to set up proper channels
|
||||||
.on(
|
// and subscribe to document changes for the specific collection
|
||||||
"postgres_changes",
|
|
||||||
{
|
// For now, we'll implement a polling mechanism as a fallback
|
||||||
event: "*",
|
// In a full migration, you'd set up Appwrite's real-time listeners
|
||||||
schema: "public",
|
const interval = setInterval(() => {
|
||||||
table: "debts",
|
fetchDebts();
|
||||||
},
|
}, 30000); // Poll every 30 seconds
|
||||||
(payload) => {
|
|
||||||
if (payload.eventType === "INSERT") {
|
|
||||||
setDebts((prev) => [payload.new as Debt, ...prev]);
|
|
||||||
} else if (payload.eventType === "UPDATE") {
|
|
||||||
setDebts((prev) =>
|
|
||||||
prev.map((debt) =>
|
|
||||||
debt.id === payload.new.id ? (payload.new as Debt) : debt
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (payload.eventType === "DELETE") {
|
|
||||||
setDebts((prev) =>
|
|
||||||
prev.filter((debt) => debt.id !== payload.old.id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
subscription.unsubscribe();
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Implement proper Appwrite real-time subscription
|
||||||
|
// client.subscribe('databases.${DATABASE_ID}.collections.${COLLECTIONS.DEBTS}.documents', response => {
|
||||||
|
// // Handle real-time updates
|
||||||
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateStats = () => {
|
const calculateStats = () => {
|
||||||
@@ -149,7 +138,7 @@ export function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
await supabase.auth.signOut();
|
await account.deleteSession('current');
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -336,10 +325,11 @@ export function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Onboarding Dialog */}
|
{/* Onboarding Dialog */}
|
||||||
<OnboardingDialog
|
{/* TODO: Migrate OnboardingDialog to Appwrite */}
|
||||||
|
{/* <OnboardingDialog
|
||||||
open={showOnboarding}
|
open={showOnboarding}
|
||||||
onComplete={handleOnboardingComplete}
|
onComplete={handleOnboardingComplete}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Eye,
|
Eye,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { supabase, type Debt, type DebtVariable } from "../lib/supabase";
|
import { account, databases, functions, DATABASE_ID, COLLECTIONS, type Debt, type DebtVariable } from "../lib/appwrite";
|
||||||
|
import { ID, Query } from "appwrite";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { formatCurrency } from "../lib/utils";
|
import { formatCurrency } from "../lib/utils";
|
||||||
import {
|
import {
|
||||||
@@ -53,8 +54,9 @@ import {
|
|||||||
getVariablesForTemplate,
|
getVariablesForTemplate,
|
||||||
updateVariablesForTextChange,
|
updateVariablesForTextChange,
|
||||||
} from "../lib/emailVariables";
|
} from "../lib/emailVariables";
|
||||||
import { ManualResponseDialog } from "./ManualResponseDialog";
|
// TODO: Migrate these components to Appwrite
|
||||||
import { ConversationTimeline } from "./ConversationTimeline";
|
// import { ManualResponseDialog } from "./ManualResponseDialog";
|
||||||
|
// import { ConversationTimeline } from "./ConversationTimeline";
|
||||||
|
|
||||||
interface DebtCardProps {
|
interface DebtCardProps {
|
||||||
debt: Debt;
|
debt: Debt;
|
||||||
@@ -217,18 +219,12 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { error } = await supabase
|
await databases.updateDocument(
|
||||||
.from("debts")
|
DATABASE_ID,
|
||||||
.update({ metadata: updatedMetadata })
|
COLLECTIONS.DEBTS,
|
||||||
.eq("id", debt.id);
|
debt.$id,
|
||||||
|
{ metadata: updatedMetadata }
|
||||||
if (error) {
|
);
|
||||||
console.error("Error saving debt metadata:", error);
|
|
||||||
toast.error("Error", {
|
|
||||||
description: "Failed to save email changes. Please try again.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save variables to database
|
// Save variables to database
|
||||||
await saveVariablesToDatabase(debt.id, variables);
|
await saveVariablesToDatabase(debt.id, variables);
|
||||||
@@ -397,17 +393,16 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
|
|
||||||
const checkServerToken = async () => {
|
const checkServerToken = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const { data: profile } = await supabase
|
const response = await databases.listDocuments(
|
||||||
.from("user_profiles")
|
DATABASE_ID,
|
||||||
.select("postmark_server_token")
|
COLLECTIONS.USER_PROFILES,
|
||||||
.eq("user_id", user.id)
|
[Query.equal('user_id', user.$id)]
|
||||||
.single();
|
);
|
||||||
|
|
||||||
|
const profile = response.documents[0];
|
||||||
setUserProfile(profile);
|
setUserProfile(profile);
|
||||||
setHasServerToken(!!profile?.postmark_server_token);
|
setHasServerToken(!!profile?.postmark_server_token);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -427,20 +422,20 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
|
|
||||||
setIsApproving(true);
|
setIsApproving(true);
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) throw new Error("User not authenticated");
|
if (!user) throw new Error("User not authenticated");
|
||||||
|
|
||||||
if (sendEmail) {
|
if (sendEmail) {
|
||||||
// Call the send-email function
|
// Call the send-email function
|
||||||
const { data, error } = await supabase.functions.invoke("send-email", {
|
const response = await functions.createExecution(
|
||||||
body: {
|
'send-email', // Function ID
|
||||||
|
JSON.stringify({
|
||||||
debtId: debt.id,
|
debtId: debt.id,
|
||||||
},
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
if (error) throw error;
|
const data = JSON.parse(response.response);
|
||||||
|
if (response.status === 'failed') throw new Error(data.error || 'Function execution failed');
|
||||||
|
|
||||||
if (data.requiresConfiguration) {
|
if (data.requiresConfiguration) {
|
||||||
toast.error("Configuration Required", {
|
toast.error("Configuration Required", {
|
||||||
@@ -455,17 +450,16 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Call the approve-debt function to handle approval without sending email
|
// Call the approve-debt function to handle approval without sending email
|
||||||
const { data, error } = await supabase.functions.invoke(
|
const response = await functions.createExecution(
|
||||||
"approve-debt",
|
'approve-debt', // Function ID
|
||||||
{
|
JSON.stringify({
|
||||||
body: {
|
debtId: debt.id,
|
||||||
debtId: debt.id,
|
approvalNote: "Approved by user without sending email",
|
||||||
approvalNote: "Approved by user without sending email",
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) throw error;
|
const data = JSON.parse(response.response);
|
||||||
|
if (response.status === 'failed') throw new Error(data.error || 'Function execution failed');
|
||||||
|
|
||||||
toast.success("Debt Approved", {
|
toast.success("Debt Approved", {
|
||||||
description: `Negotiation for ${data.vendor} has been approved and saved.`,
|
description: `Negotiation for ${data.vendor} has been approved and saved.`,
|
||||||
@@ -490,9 +484,11 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
const handleReject = async () => {
|
const handleReject = async () => {
|
||||||
setIsRejecting(true);
|
setIsRejecting(true);
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
await databases.updateDocument(
|
||||||
.from("debts")
|
DATABASE_ID,
|
||||||
.update({
|
COLLECTIONS.DEBTS,
|
||||||
|
debt.id,
|
||||||
|
{
|
||||||
status: "opted_out",
|
status: "opted_out",
|
||||||
metadata: {
|
metadata: {
|
||||||
...debt.metadata,
|
...debt.metadata,
|
||||||
@@ -501,20 +497,25 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
reason: "User rejected negotiation",
|
reason: "User rejected negotiation",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
updated_at: new Date().toISOString(),
|
||||||
.eq("id", debt.id);
|
}
|
||||||
|
);
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
// Log the action
|
// Log the action
|
||||||
await supabase.from("audit_logs").insert({
|
await databases.createDocument(
|
||||||
debt_id: debt.id,
|
DATABASE_ID,
|
||||||
action: "negotiation_rejected",
|
COLLECTIONS.AUDIT_LOGS,
|
||||||
details: {
|
ID.unique(),
|
||||||
rejectedAt: new Date().toISOString(),
|
{
|
||||||
reason: "User rejected negotiation",
|
debt_id: debt.id,
|
||||||
},
|
action: "negotiation_rejected",
|
||||||
});
|
details: {
|
||||||
|
rejectedAt: new Date().toISOString(),
|
||||||
|
reason: "User rejected negotiation",
|
||||||
|
},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
toast.success("Negotiation Rejected", {
|
toast.success("Negotiation Rejected", {
|
||||||
description: "The negotiation has been marked as rejected.",
|
description: "The negotiation has been marked as rejected.",
|
||||||
@@ -618,9 +619,10 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Manual Response Dialog - show when requires manual review */}
|
{/* Manual Response Dialog - show when requires manual review */}
|
||||||
{debt.status === "requires_manual_review" && (
|
{/* TODO: Migrate ManualResponseDialog to Appwrite */}
|
||||||
|
{/* {debt.status === "requires_manual_review" && (
|
||||||
<ManualResponseDialog debt={debt} onResponseSent={onUpdate} />
|
<ManualResponseDialog debt={debt} onResponseSent={onUpdate} />
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{/* Approve/Reject Buttons */}
|
{/* Approve/Reject Buttons */}
|
||||||
{showApproveRejectButtons() && (
|
{showApproveRejectButtons() && (
|
||||||
@@ -731,12 +733,13 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ConversationTimeline
|
{/* TODO: Migrate ConversationTimeline to Appwrite */}
|
||||||
|
{/* <ConversationTimeline
|
||||||
debt={debt}
|
debt={debt}
|
||||||
onDebtUpdate={(debt) => {
|
onDebtUpdate={(debt) => {
|
||||||
setDebts(debts.map((d) => (d.id === debt.id ? debt : d)));
|
setDebts(debts.map((d) => (d.id === debt.id ? debt : d)));
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { supabase } from "../lib/supabase";
|
import { account } from "../lib/appwrite";
|
||||||
import type { User } from "@supabase/supabase-js";
|
import type { Models } from "appwrite";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -14,25 +14,25 @@ import { BarChart3, LogOut, User as UserIcon, Settings } from "lucide-react";
|
|||||||
import { ModeToggle } from "./ModeToggle";
|
import { ModeToggle } from "./ModeToggle";
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
account.get().then((currentUser) => {
|
||||||
setUser(session?.user ?? null);
|
setUser(currentUser);
|
||||||
|
}).catch(() => {
|
||||||
|
setUser(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
data: { subscription },
|
|
||||||
} = supabase.auth.onAuthStateChange((event, session) => {
|
|
||||||
setUser(session?.user ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => subscription.unsubscribe();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
await supabase.auth.signOut();
|
try {
|
||||||
window.location.href = "/";
|
await account.deleteSession('current');
|
||||||
|
window.location.href = "/";
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Sign out error:', error);
|
||||||
|
// Force redirect even if sign out fails
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitials = (email: string) => {
|
const getInitials = (email: string) => {
|
||||||
|
|||||||
173
src/lib/appwrite-admin.ts
Normal file
173
src/lib/appwrite-admin.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { Client, Account, Databases, Functions } from "appwrite";
|
||||||
|
import { DATABASE_ID, COLLECTIONS } from "./appwrite";
|
||||||
|
|
||||||
|
// Export constants for server-side operations
|
||||||
|
export { DATABASE_ID, COLLECTIONS };
|
||||||
|
|
||||||
|
// Create admin client instances for server-side use
|
||||||
|
const adminClient = createAppwriteAdmin();
|
||||||
|
export const databases = adminClient.databases;
|
||||||
|
export const functions = adminClient.functions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Appwrite client with admin privileges for server-side operations
|
||||||
|
* This client should only be used in trusted contexts like webhooks, API routes, and server-side functions
|
||||||
|
*/
|
||||||
|
export function createAppwriteAdmin() {
|
||||||
|
const appwriteEndpoint = process.env.PUBLIC_APPWRITE_ENDPOINT || import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
|
||||||
|
const appwriteProjectId = process.env.PUBLIC_APPWRITE_PROJECT_ID || import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
|
||||||
|
const appwriteApiKey = process.env.APPWRITE_API_KEY || import.meta.env.APPWRITE_API_KEY;
|
||||||
|
|
||||||
|
if (!appwriteEndpoint || !appwriteProjectId || !appwriteApiKey) {
|
||||||
|
throw new Error("Missing Appwrite configuration for admin operations");
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client()
|
||||||
|
.setEndpoint(appwriteEndpoint)
|
||||||
|
.setProject(appwriteProjectId)
|
||||||
|
.setKey(appwriteApiKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
account: new Account(client),
|
||||||
|
databases: new Databases(client),
|
||||||
|
functions: new Functions(client)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle database errors with more user-friendly messages
|
||||||
|
*/
|
||||||
|
export function handleDatabaseError(error: any) {
|
||||||
|
let errorMessage = error.message;
|
||||||
|
|
||||||
|
if (error.message.includes("permission")) {
|
||||||
|
errorMessage = "Database access denied - please check permissions";
|
||||||
|
} else if (error.message.includes("duplicate")) {
|
||||||
|
errorMessage = "Duplicate entry detected";
|
||||||
|
} else if (error.message.includes("not found")) {
|
||||||
|
errorMessage = "Resource not found";
|
||||||
|
} else if (error.message.includes("required")) {
|
||||||
|
errorMessage = "Required field is missing";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: errorMessage,
|
||||||
|
originalError: process.env.NODE_ENV === "development" ? error : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user ID by email address in Appwrite
|
||||||
|
* Searches through users collection by email
|
||||||
|
*/
|
||||||
|
export async function getUserIdByEmail(
|
||||||
|
email: string,
|
||||||
|
adminClient?: ReturnType<typeof createAppwriteAdmin>
|
||||||
|
): Promise<string | null> {
|
||||||
|
const client = adminClient || createAppwriteAdmin();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Query users by email - assuming users collection exists
|
||||||
|
const response = await client.databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
'users', // This would be the users collection ID in Appwrite
|
||||||
|
[
|
||||||
|
// Appwrite uses Query objects for filtering
|
||||||
|
// Note: This will need to be adjusted based on actual Appwrite schema
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter results by email since Appwrite queries might be different
|
||||||
|
const user = response.documents.find(user =>
|
||||||
|
user.email.toLowerCase() === email.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return user.$id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in main users, check additional emails if that collection exists
|
||||||
|
try {
|
||||||
|
const additionalEmailsResponse = await client.databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const additionalEmail = additionalEmailsResponse.documents.find(email_doc =>
|
||||||
|
email_doc.email_address.toLowerCase() === email.toLowerCase() &&
|
||||||
|
email_doc.verified === true
|
||||||
|
);
|
||||||
|
|
||||||
|
return additionalEmail?.user_id || null;
|
||||||
|
} catch (additionalError) {
|
||||||
|
console.error("Error finding user by additional email:", additionalError);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in getUserIdByEmail:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full user information by email address
|
||||||
|
*/
|
||||||
|
export async function getUserByEmail(
|
||||||
|
email: string,
|
||||||
|
adminClient?: ReturnType<typeof createAppwriteAdmin>
|
||||||
|
) {
|
||||||
|
const client = adminClient || createAppwriteAdmin();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Query users by email
|
||||||
|
const response = await client.databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
'users',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = response.documents.find(user =>
|
||||||
|
user.email.toLowerCase() === email.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check additional emails with user join
|
||||||
|
try {
|
||||||
|
const additionalEmailsResponse = await client.databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const additionalEmail = additionalEmailsResponse.documents.find(email_doc =>
|
||||||
|
email_doc.email_address.toLowerCase() === email.toLowerCase() &&
|
||||||
|
email_doc.verified === true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (additionalEmail) {
|
||||||
|
// Get the user record by user_id
|
||||||
|
const userResponse = await client.databases.getDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
'users',
|
||||||
|
additionalEmail.user_id
|
||||||
|
);
|
||||||
|
return userResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (additionalError) {
|
||||||
|
console.error("Error finding user by additional email:", additionalError);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in getUserByEmail:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/lib/appwrite.ts
Normal file
143
src/lib/appwrite.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { Client, Account, Databases, Functions } from "appwrite";
|
||||||
|
|
||||||
|
const appwriteEndpoint = import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
|
||||||
|
const appwriteProjectId = import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
|
||||||
|
|
||||||
|
if (!appwriteEndpoint || !appwriteProjectId) {
|
||||||
|
throw new Error("Missing Appwrite environment variables");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const client = new Client()
|
||||||
|
.setEndpoint(appwriteEndpoint)
|
||||||
|
.setProject(appwriteProjectId);
|
||||||
|
|
||||||
|
export const account = new Account(client);
|
||||||
|
export const databases = new Databases(client);
|
||||||
|
export const functions = new Functions(client);
|
||||||
|
|
||||||
|
// Database and collection IDs (to be configured in Appwrite)
|
||||||
|
export const DATABASE_ID = import.meta.env.PUBLIC_APPWRITE_DATABASE_ID || "inbox-negotiator-db";
|
||||||
|
export const COLLECTIONS = {
|
||||||
|
DEBTS: "debts",
|
||||||
|
AUDIT_LOGS: "audit_logs",
|
||||||
|
USER_PROFILES: "user_profiles",
|
||||||
|
ADDITIONAL_EMAILS: "additional_emails",
|
||||||
|
EMAIL_PROCESSING_USAGE: "email_processing_usage",
|
||||||
|
DEBT_VARIABLES: "debt_variables",
|
||||||
|
CONVERSATION_MESSAGES: "conversation_messages"
|
||||||
|
};
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Debt = {
|
||||||
|
$id: string;
|
||||||
|
id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
vendor: string;
|
||||||
|
amount: number;
|
||||||
|
raw_email: string | null;
|
||||||
|
status:
|
||||||
|
| "received"
|
||||||
|
| "negotiating"
|
||||||
|
| "approved"
|
||||||
|
| "sent"
|
||||||
|
| "awaiting_response"
|
||||||
|
| "counter_negotiating"
|
||||||
|
| "requires_manual_review"
|
||||||
|
| "accepted"
|
||||||
|
| "rejected"
|
||||||
|
| "settled"
|
||||||
|
| "failed"
|
||||||
|
| "opted_out";
|
||||||
|
negotiated_plan: string | null;
|
||||||
|
projected_savings: number;
|
||||||
|
user_id: string;
|
||||||
|
description?: string | null;
|
||||||
|
due_date?: string | null;
|
||||||
|
conversation_count?: number;
|
||||||
|
last_message_at?: string;
|
||||||
|
negotiation_round?: number;
|
||||||
|
prospected_savings?: number;
|
||||||
|
actual_savings?: number;
|
||||||
|
metadata?: Record<string, any> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuditLog = {
|
||||||
|
$id: string;
|
||||||
|
id: string;
|
||||||
|
created_at: string;
|
||||||
|
debt_id: string;
|
||||||
|
action: string;
|
||||||
|
details: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserProfile = {
|
||||||
|
$id: string;
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
onboarding_completed: boolean;
|
||||||
|
first_login_at: string | null;
|
||||||
|
email_processing_limit: number;
|
||||||
|
postmark_server_token: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdditionalEmail = {
|
||||||
|
$id: string;
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
email_address: string;
|
||||||
|
verified: boolean;
|
||||||
|
verification_token: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmailProcessingUsage = {
|
||||||
|
$id: string;
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
month_year: string;
|
||||||
|
emails_processed: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DebtVariable = {
|
||||||
|
$id: string;
|
||||||
|
id: string;
|
||||||
|
debt_id: string;
|
||||||
|
variable_name: string;
|
||||||
|
variable_value: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConversationMessage = {
|
||||||
|
$id: string;
|
||||||
|
id: string;
|
||||||
|
debt_id: string;
|
||||||
|
message_type:
|
||||||
|
| "initial_debt"
|
||||||
|
| "negotiation_sent"
|
||||||
|
| "response_received"
|
||||||
|
| "counter_offer"
|
||||||
|
| "acceptance"
|
||||||
|
| "rejection"
|
||||||
|
| "manual_response";
|
||||||
|
direction: "inbound" | "outbound";
|
||||||
|
subject?: string;
|
||||||
|
body: string;
|
||||||
|
from_email?: string;
|
||||||
|
to_email?: string;
|
||||||
|
message_id?: string;
|
||||||
|
ai_analysis?: Record<string, any>;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
* - Processing complete email templates
|
* - Processing complete email templates
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { supabase } from "./supabase";
|
import { databases, DATABASE_ID, COLLECTIONS } from "./appwrite-admin";
|
||||||
|
import { ID } from "appwrite";
|
||||||
|
|
||||||
export interface VariableProcessingResult {
|
export interface VariableProcessingResult {
|
||||||
processedSubject: string;
|
processedSubject: string;
|
||||||
@@ -68,18 +69,14 @@ export async function loadVariablesFromDatabase(
|
|||||||
debtId: string
|
debtId: string
|
||||||
): Promise<Record<string, string>> {
|
): Promise<Record<string, string>> {
|
||||||
try {
|
try {
|
||||||
const { data: dbVariables, error } = await supabase
|
const response = await databases.listDocuments(
|
||||||
.from("debt_variables")
|
DATABASE_ID,
|
||||||
.select("variable_name, variable_value")
|
COLLECTIONS.DEBT_VARIABLES,
|
||||||
.eq("debt_id", debtId);
|
[`debt_id="${debtId}"`]
|
||||||
|
);
|
||||||
if (error) {
|
|
||||||
console.error("Error loading variables from database:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadedVariables: Record<string, string> = {};
|
const loadedVariables: Record<string, string> = {};
|
||||||
dbVariables?.forEach((dbVar) => {
|
response.documents.forEach((dbVar: any) => {
|
||||||
loadedVariables[dbVar.variable_name] = dbVar.variable_value || "";
|
loadedVariables[dbVar.variable_name] = dbVar.variable_value || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,33 +97,34 @@ export async function saveVariablesToDatabase(
|
|||||||
variables: Record<string, string>
|
variables: Record<string, string>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// First, delete existing variables for this debt
|
// First, get existing variables for this debt
|
||||||
const { error: deleteError } = await supabase
|
const existingVariables = await databases.listDocuments(
|
||||||
.from("debt_variables")
|
DATABASE_ID,
|
||||||
.delete()
|
COLLECTIONS.DEBT_VARIABLES,
|
||||||
.eq("debt_id", debtId);
|
[`debt_id="${debtId}"`]
|
||||||
|
);
|
||||||
|
|
||||||
if (deleteError) {
|
// Delete existing variables
|
||||||
console.error("Error deleting existing variables:", deleteError);
|
for (const variable of existingVariables.documents) {
|
||||||
throw deleteError;
|
await databases.deleteDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.DEBT_VARIABLES,
|
||||||
|
variable.$id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then insert new variables
|
// Insert new variables
|
||||||
const variableRecords = Object.entries(variables).map(([name, value]) => ({
|
for (const [name, value] of Object.entries(variables)) {
|
||||||
debt_id: debtId,
|
await databases.createDocument(
|
||||||
variable_name: name,
|
DATABASE_ID,
|
||||||
variable_value: value,
|
COLLECTIONS.DEBT_VARIABLES,
|
||||||
}));
|
ID.unique(),
|
||||||
|
{
|
||||||
if (variableRecords.length > 0) {
|
debt_id: debtId,
|
||||||
const { error: insertError } = await supabase
|
variable_name: name,
|
||||||
.from("debt_variables")
|
variable_value: value,
|
||||||
.insert(variableRecords);
|
}
|
||||||
|
);
|
||||||
if (insertError) {
|
|
||||||
console.error("Error inserting variables:", insertError);
|
|
||||||
throw insertError;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in saveVariablesToDatabase:", error);
|
console.error("Error in saveVariablesToDatabase:", error);
|
||||||
|
|||||||
@@ -1,153 +1,23 @@
|
|||||||
import { createClient } from "@supabase/supabase-js";
|
// This file has been migrated to appwrite-admin.ts
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
// The original Supabase admin functionality is now handled by Appwrite
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Supabase client with service role key for server-side operations
|
* @deprecated Use appwrite-admin.ts instead
|
||||||
* This client bypasses Row Level Security (RLS) and should only be used in trusted contexts
|
* This file contained Supabase admin client functionality that has been migrated to Appwrite
|
||||||
* like webhooks, API routes, and server-side functions
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function createSupabaseAdmin() {
|
export function createSupabaseAdmin() {
|
||||||
const supabaseUrl =
|
throw new Error('This function has been migrated to Appwrite. Use appwrite-admin.ts instead.');
|
||||||
process.env.PUBLIC_SUPABASE_URL || import.meta.env.PUBLIC_SUPABASE_URL;
|
|
||||||
const supabaseServiceKey =
|
|
||||||
process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
|
||||||
import.meta.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
||||||
|
|
||||||
console.log({ supabaseUrl, supabaseServiceKey });
|
|
||||||
|
|
||||||
if (!supabaseUrl || !supabaseServiceKey) {
|
|
||||||
throw new Error(
|
|
||||||
"Missing Supabase URL or Service Role Key for admin operations"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createClient(supabaseUrl, supabaseServiceKey, {
|
|
||||||
auth: {
|
|
||||||
autoRefreshToken: false,
|
|
||||||
persistSession: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle database errors with more user-friendly messages
|
|
||||||
*/
|
|
||||||
export function handleDatabaseError(error: any) {
|
export function handleDatabaseError(error: any) {
|
||||||
let errorMessage = error.message;
|
throw new Error('This function has been migrated to Appwrite. Use appwrite-admin.ts instead.');
|
||||||
|
|
||||||
if (error.message.includes("row-level security")) {
|
|
||||||
errorMessage = "Database access denied - please check RLS policies";
|
|
||||||
} else if (error.message.includes("duplicate key")) {
|
|
||||||
errorMessage = "Duplicate entry detected";
|
|
||||||
} else if (error.message.includes("foreign key")) {
|
|
||||||
errorMessage = "Invalid reference in data";
|
|
||||||
} else if (error.message.includes("not null")) {
|
|
||||||
errorMessage = "Required field is missing";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: errorMessage,
|
|
||||||
originalError: process.env.NODE_ENV === "development" ? error : undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function getUserIdByEmail(email: string): Promise<string | null> {
|
||||||
* Find user ID by email address (primary or additional email)
|
throw new Error('This function has been migrated to Appwrite. Use appwrite-admin.ts instead.');
|
||||||
* First checks public.users table, then additional_emails if needed
|
|
||||||
*/
|
|
||||||
export async function getUserIdByEmail(
|
|
||||||
email: string,
|
|
||||||
supabaseAdmin?: SupabaseClient
|
|
||||||
): Promise<string | null> {
|
|
||||||
const client = supabaseAdmin || createSupabaseAdmin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First try to find user by primary email in public.users table
|
|
||||||
const { data: primaryUser, error: primaryError } = await client
|
|
||||||
.from("users")
|
|
||||||
.select("id")
|
|
||||||
.eq("email", email.toLowerCase())
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (primaryError) {
|
|
||||||
console.error("Error finding user by primary email:", primaryError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (primaryUser) {
|
|
||||||
return primaryUser.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found, check additional emails
|
|
||||||
const { data: additionalEmail, error: additionalError } = await client
|
|
||||||
.from("additional_emails")
|
|
||||||
.select("user_id")
|
|
||||||
.eq("email_address", email.toLowerCase())
|
|
||||||
// TODO: START REQUIRING VERIFIED ADDITIONAL EMAILS
|
|
||||||
// .eq("verified", true)
|
|
||||||
.eq("verified", false)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (additionalError) {
|
|
||||||
console.error("Error finding user by additional email:", additionalError);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return additionalEmail?.user_id || null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in getUserIdByEmail:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function getUserByEmail(email: string) {
|
||||||
* Get full user information by email address (primary or additional email)
|
throw new Error('This function has been migrated to Appwrite. Use appwrite-admin.ts instead.');
|
||||||
* First checks public.users table, then additional_emails if needed
|
|
||||||
*/
|
|
||||||
export async function getUserByEmail(
|
|
||||||
email: string,
|
|
||||||
supabaseAdmin?: SupabaseClient
|
|
||||||
) {
|
|
||||||
const client = supabaseAdmin || createSupabaseAdmin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First try to find user by primary email in public.users table
|
|
||||||
const { data: primaryUser, error: primaryError } = await client
|
|
||||||
.from("users")
|
|
||||||
.select("*")
|
|
||||||
.eq("email", email.toLowerCase())
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (primaryError) {
|
|
||||||
console.error("Error finding user by primary email:", primaryError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (primaryUser) {
|
|
||||||
return primaryUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found, check additional emails and join with users table
|
|
||||||
const { data: userViaAdditionalEmail, error: additionalError } = await client
|
|
||||||
.from("additional_emails")
|
|
||||||
.select(`
|
|
||||||
user_id,
|
|
||||||
users!inner (
|
|
||||||
id,
|
|
||||||
email,
|
|
||||||
created_at
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.eq("email_address", email.toLowerCase())
|
|
||||||
.eq("verified", true)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (additionalError) {
|
|
||||||
console.error("Error finding user by additional email:", additionalError);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userViaAdditionalEmail?.users || null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in getUserByEmail:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { createClient } from "@supabase/supabase-js";
|
// Type definitions for Appwrite migration
|
||||||
|
// This file no longer contains Supabase client - use appwrite.ts instead
|
||||||
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL;
|
|
||||||
const supabaseAnonKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
|
|
||||||
|
|
||||||
if (!supabaseUrl || !supabaseAnonKey) {
|
|
||||||
throw new Error("Missing Supabase environment variables");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import {
|
import {
|
||||||
createSupabaseAdmin,
|
createAppwriteAdmin,
|
||||||
getUserIdByEmail,
|
getUserIdByEmail,
|
||||||
handleDatabaseError,
|
handleDatabaseError,
|
||||||
} from "../../lib/supabase-admin";
|
} from "../../lib/appwrite-admin";
|
||||||
import { generateObject } from "ai";
|
import { generateObject } from "ai";
|
||||||
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
import { DATABASE_ID, COLLECTIONS } from "../../lib/appwrite";
|
||||||
|
import { ID, Query } from "appwrite";
|
||||||
|
|
||||||
// Schema for debt information extraction
|
// Schema for debt information extraction
|
||||||
const debtSchema = z.object({
|
const debtSchema = z.object({
|
||||||
@@ -125,19 +126,52 @@ async function parseDebtWithAI(emailText: string, fromEmail: string) {
|
|||||||
// Function to increment email processing usage
|
// Function to increment email processing usage
|
||||||
async function incrementEmailUsage(
|
async function incrementEmailUsage(
|
||||||
userId: string,
|
userId: string,
|
||||||
supabaseAdmin: SupabaseClient,
|
appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Call the database function to increment usage
|
// In Appwrite, we'll need to implement this differently since there are no stored procedures
|
||||||
const { error } = await supabaseAdmin.rpc("increment_email_usage", {
|
// For now, we'll implement a simple increment by finding the current month's usage and updating it
|
||||||
target_user_id: userId,
|
|
||||||
});
|
const currentDate = new Date();
|
||||||
|
const monthYear = `${currentDate.getFullYear()}-${(currentDate.getMonth() + 1).toString().padStart(2, '0')}`;
|
||||||
if (error) {
|
|
||||||
console.error("Error incrementing email usage:", error);
|
// Get current usage for this month
|
||||||
|
const response = await appwriteAdmin.databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||||
|
[Query.equal('user_id', userId), Query.equal('month_year', monthYear)]
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingUsage = response.documents[0];
|
||||||
|
|
||||||
|
if (existingUsage) {
|
||||||
|
// Update existing usage
|
||||||
|
await appwriteAdmin.databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||||
|
existingUsage.$id,
|
||||||
|
{
|
||||||
|
emails_processed: existingUsage.emails_processed + 1,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Create new usage record
|
||||||
|
await appwriteAdmin.databases.createDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||||
|
ID.unique(),
|
||||||
|
{
|
||||||
|
user_id: userId,
|
||||||
|
month_year: monthYear,
|
||||||
|
emails_processed: 1,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error calling increment_email_usage:", error);
|
console.error("Error incrementing email usage:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,25 +179,25 @@ async function incrementEmailUsage(
|
|||||||
async function checkForExistingNegotiation(
|
async function checkForExistingNegotiation(
|
||||||
fromEmail: string,
|
fromEmail: string,
|
||||||
toEmail: string,
|
toEmail: string,
|
||||||
supabaseAdmin: any,
|
appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Look for debts where we've sent emails to this fromEmail and are awaiting response
|
// Look for debts where we've sent emails to this fromEmail and are awaiting response
|
||||||
// Include multiple statuses that indicate we're in an active negotiation
|
const response = await appwriteAdmin.databases.listDocuments(
|
||||||
const { data: debts, error } = await supabaseAdmin
|
DATABASE_ID,
|
||||||
.from("debts")
|
COLLECTIONS.DEBTS,
|
||||||
.select("*")
|
[Query.in('status', ['sent', 'awaiting_response', 'counter_negotiating']), Query.orderDesc('last_message_at')]
|
||||||
.in("status", ["sent", "awaiting_response", "counter_negotiating"])
|
);
|
||||||
.contains("metadata", { fromEmail: fromEmail, toEmail: toEmail })
|
|
||||||
.order("last_message_at", { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
// Find matching debts based on email metadata
|
||||||
console.error("Error checking for existing negotiation:", error);
|
const matchingDebts = response.documents.filter(debt => {
|
||||||
return null;
|
const metadata = debt.metadata as any;
|
||||||
}
|
return metadata?.fromEmail === fromEmail &&
|
||||||
|
metadata?.toEmail === toEmail;
|
||||||
|
});
|
||||||
|
|
||||||
// Return the most recent debt that matches
|
// Return the most recent debt that matches (already sorted by orderDesc in query)
|
||||||
return debts && debts.length > 0 ? debts[0] : null;
|
return matchingDebts.length > 0 ? matchingDebts[0] : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in checkForExistingNegotiation:", error);
|
console.error("Error in checkForExistingNegotiation:", error);
|
||||||
return null;
|
return null;
|
||||||
@@ -174,7 +208,7 @@ async function checkForExistingNegotiation(
|
|||||||
async function handleNegotiationResponse(
|
async function handleNegotiationResponse(
|
||||||
debt: any,
|
debt: any,
|
||||||
emailData: any,
|
emailData: any,
|
||||||
supabaseAdmin: any,
|
appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const textBody = emailData.TextBody || emailData.HtmlBody || "";
|
const textBody = emailData.TextBody || emailData.HtmlBody || "";
|
||||||
@@ -183,45 +217,58 @@ async function handleNegotiationResponse(
|
|||||||
const messageId = emailData.MessageID || `inbound-${Date.now()}`;
|
const messageId = emailData.MessageID || `inbound-${Date.now()}`;
|
||||||
|
|
||||||
// First, record this message in the conversation
|
// First, record this message in the conversation
|
||||||
await supabaseAdmin.from("conversation_messages").insert({
|
await appwriteAdmin.databases.createDocument(
|
||||||
debt_id: debt.id,
|
DATABASE_ID,
|
||||||
message_type: "response_received",
|
COLLECTIONS.CONVERSATION_MESSAGES,
|
||||||
direction: "inbound",
|
ID.unique(),
|
||||||
subject: subject,
|
{
|
||||||
body: textBody,
|
debt_id: debt.$id,
|
||||||
from_email: fromEmail,
|
message_type: "response_received",
|
||||||
to_email: emailData.ToFull?.[0]?.Email || emailData.To || "",
|
direction: "inbound",
|
||||||
message_id: messageId,
|
subject: subject,
|
||||||
});
|
body: textBody,
|
||||||
|
from_email: fromEmail,
|
||||||
|
to_email: emailData.ToFull?.[0]?.Email || emailData.To || "",
|
||||||
|
message_id: messageId,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Update debt conversation tracking
|
// Update debt conversation tracking
|
||||||
await supabaseAdmin
|
await appwriteAdmin.databases.updateDocument(
|
||||||
.from("debts")
|
DATABASE_ID,
|
||||||
.update({
|
COLLECTIONS.DEBTS,
|
||||||
|
debt.$id,
|
||||||
|
{
|
||||||
conversation_count: debt.conversation_count + 1,
|
conversation_count: debt.conversation_count + 1,
|
||||||
last_message_at: new Date().toISOString(),
|
last_message_at: new Date().toISOString(),
|
||||||
status: "counter_negotiating", // Temporary status while analyzing
|
status: "counter_negotiating", // Temporary status while analyzing
|
||||||
})
|
updated_at: new Date().toISOString()
|
||||||
.eq("id", debt.id);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Call the analyze-response function
|
// Call the analyze-response function
|
||||||
const supabaseUrl = process.env.SUPABASE_URL ||
|
const appwriteEndpoint = process.env.PUBLIC_APPWRITE_ENDPOINT ||
|
||||||
import.meta.env.PUBLIC_SUPABASE_URL;
|
import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
|
||||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
const appwriteProjectId = process.env.PUBLIC_APPWRITE_PROJECT_ID ||
|
||||||
import.meta.env.SUPABASE_SERVICE_ROLE_KEY;
|
import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
|
||||||
|
const appwriteApiKey = process.env.APPWRITE_API_KEY ||
|
||||||
|
import.meta.env.APPWRITE_API_KEY;
|
||||||
|
|
||||||
if (supabaseUrl && supabaseServiceKey) {
|
if (appwriteEndpoint && appwriteProjectId && appwriteApiKey) {
|
||||||
const analyzeUrl = `${supabaseUrl}/functions/v1/analyze-response`;
|
const analyzeUrl = `${appwriteEndpoint}/functions/v1/analyze-response`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(analyzeUrl, {
|
const response = await fetch(analyzeUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${supabaseServiceKey}`,
|
"X-Appwrite-Project": appwriteProjectId,
|
||||||
|
"X-Appwrite-Key": appwriteApiKey,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
debtId: debt.id,
|
debtId: debt.$id,
|
||||||
fromEmail,
|
fromEmail,
|
||||||
subject,
|
subject,
|
||||||
body: textBody,
|
body: textBody,
|
||||||
@@ -233,20 +280,6 @@ async function handleNegotiationResponse(
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("Response analysis completed:", result);
|
console.log("Response analysis completed:", result);
|
||||||
|
|
||||||
// Update the conversation message with AI analysis
|
|
||||||
// !MAYBE NEEDED
|
|
||||||
// await supabaseAdmin
|
|
||||||
// .from("conversation_messages")
|
|
||||||
// .update({
|
|
||||||
// ai_analysis: result.analysis,
|
|
||||||
// message_type: result.analysis?.intent === "acceptance"
|
|
||||||
// ? "acceptance"
|
|
||||||
// : result.analysis?.intent === "rejection"
|
|
||||||
// ? "rejection"
|
|
||||||
// : "response_received",
|
|
||||||
// })
|
|
||||||
// .eq("message_id", messageId);
|
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -270,22 +303,22 @@ async function handleNegotiationResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: just log the response and mark for manual review
|
// Fallback: just log the response and mark for manual review
|
||||||
await supabaseAdmin.from("audit_logs").insert({
|
await appwriteAdmin.databases.createDocument(
|
||||||
debt_id: debt.id,
|
DATABASE_ID,
|
||||||
action: "response_received_fallback",
|
COLLECTIONS.AUDIT_LOGS,
|
||||||
details: {
|
ID.unique(),
|
||||||
fromEmail,
|
{
|
||||||
subject,
|
debt_id: debt.$id,
|
||||||
bodyPreview: textBody.substring(0, 200),
|
action: "response_received_fallback",
|
||||||
requiresManualReview: true,
|
details: {
|
||||||
},
|
fromEmail,
|
||||||
});
|
subject,
|
||||||
|
bodyPreview: textBody.substring(0, 200),
|
||||||
// Update status to require user review
|
requiresManualReview: true,
|
||||||
// await supabaseAdmin
|
},
|
||||||
// .from("debts")
|
created_at: new Date().toISOString()
|
||||||
// .update({ status: "awaiting_response" })
|
}
|
||||||
// .eq("id", debt.id);
|
);
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ success: true, message: "Response logged" }),
|
JSON.stringify({ success: true, message: "Response logged" }),
|
||||||
@@ -308,12 +341,12 @@ async function handleNegotiationResponse(
|
|||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
// Create service role client for webhook operations (bypasses RLS)
|
// Create admin client for webhook operations
|
||||||
let supabaseAdmin;
|
let appwriteAdmin;
|
||||||
try {
|
try {
|
||||||
supabaseAdmin = createSupabaseAdmin();
|
appwriteAdmin = createAppwriteAdmin();
|
||||||
} catch (configError) {
|
} catch (configError) {
|
||||||
console.error("Supabase admin configuration error:", configError);
|
console.error("Appwrite admin configuration error:", configError);
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: "Server configuration error" }),
|
JSON.stringify({ error: "Server configuration error" }),
|
||||||
{
|
{
|
||||||
@@ -339,7 +372,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
const toEmail = data.ToFull?.[0]?.Email || data.To || "";
|
const toEmail = data.ToFull?.[0]?.Email || data.To || "";
|
||||||
|
|
||||||
// Find the user who should receive this debt
|
// Find the user who should receive this debt
|
||||||
const userId = await getUserIdByEmail(toEmail, supabaseAdmin);
|
const userId = await getUserIdByEmail(toEmail, appwriteAdmin);
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.warn(`No user found for email: ${toEmail}`);
|
console.warn(`No user found for email: ${toEmail}`);
|
||||||
return new Response("No matching user found", { status: 200 });
|
return new Response("No matching user found", { status: 200 });
|
||||||
@@ -349,19 +382,19 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
const existingDebt = await checkForExistingNegotiation(
|
const existingDebt = await checkForExistingNegotiation(
|
||||||
fromEmail,
|
fromEmail,
|
||||||
toEmail,
|
toEmail,
|
||||||
supabaseAdmin,
|
appwriteAdmin,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log({ existingDebt, fromEmail, toEmail });
|
console.log({ existingDebt, fromEmail, toEmail });
|
||||||
if (existingDebt) {
|
if (existingDebt) {
|
||||||
console.log(
|
console.log(
|
||||||
`Found existing negotiation for debt ${existingDebt.id}, analyzing response...`,
|
`Found existing negotiation for debt ${existingDebt.$id}, analyzing response...`,
|
||||||
);
|
);
|
||||||
return await handleNegotiationResponse(existingDebt, data, supabaseAdmin);
|
return await handleNegotiationResponse(existingDebt, data, appwriteAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment email processing usage
|
// Increment email processing usage
|
||||||
await incrementEmailUsage(userId, supabaseAdmin);
|
await incrementEmailUsage(userId, appwriteAdmin);
|
||||||
|
|
||||||
// Check for opt-out using AI
|
// Check for opt-out using AI
|
||||||
const optOutDetection = await detectOptOutWithAI(textBody, fromEmail);
|
const optOutDetection = await detectOptOutWithAI(textBody, fromEmail);
|
||||||
@@ -383,15 +416,22 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
if (hasOptOut) {
|
if (hasOptOut) {
|
||||||
// Log opt-out and don't process further
|
// Log opt-out and don't process further
|
||||||
const { error } = await supabaseAdmin.from("debts").insert({
|
try {
|
||||||
user_id: userId,
|
await appwriteAdmin.databases.createDocument(
|
||||||
vendor: fromEmail,
|
DATABASE_ID,
|
||||||
amount: 0,
|
COLLECTIONS.DEBTS,
|
||||||
raw_email: textBody,
|
ID.unique(),
|
||||||
status: "opted_out",
|
{
|
||||||
});
|
user_id: userId,
|
||||||
|
vendor: fromEmail,
|
||||||
if (error) {
|
amount: 0,
|
||||||
|
raw_email: textBody,
|
||||||
|
status: "opted_out",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
console.error("Error logging opt-out:", error);
|
console.error("Error logging opt-out:", error);
|
||||||
const errorInfo = handleDatabaseError(error);
|
const errorInfo = handleDatabaseError(error);
|
||||||
return new Response(JSON.stringify({ error: errorInfo.message }), {
|
return new Response(JSON.stringify({ error: errorInfo.message }), {
|
||||||
@@ -418,44 +458,54 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert debt record with AI-extracted information
|
// Insert debt record with AI-extracted information
|
||||||
const { data: insertedDebt, error: insertError } = await supabaseAdmin
|
let insertedDebt;
|
||||||
.from("debts")
|
try {
|
||||||
.insert({
|
insertedDebt = await appwriteAdmin.databases.createDocument(
|
||||||
user_id: userId,
|
DATABASE_ID,
|
||||||
vendor: debtInfo.vendor,
|
COLLECTIONS.DEBTS,
|
||||||
amount: debtInfo.amount,
|
ID.unique(),
|
||||||
raw_email: textBody,
|
{
|
||||||
status: "received",
|
user_id: userId,
|
||||||
description: debtInfo.description,
|
vendor: debtInfo.vendor,
|
||||||
due_date: debtInfo.dueDate,
|
amount: debtInfo.amount,
|
||||||
conversation_count: 1,
|
raw_email: textBody,
|
||||||
last_message_at: new Date().toISOString(),
|
status: "received",
|
||||||
negotiation_round: 1,
|
description: debtInfo.description,
|
||||||
metadata: {
|
due_date: debtInfo.dueDate,
|
||||||
isDebtCollection: debtInfo.isDebtCollection,
|
conversation_count: 1,
|
||||||
subject: data.Subject,
|
last_message_at: new Date().toISOString(),
|
||||||
fromEmail: fromEmail,
|
negotiation_round: 1,
|
||||||
toEmail: toEmail,
|
projected_savings: 0,
|
||||||
},
|
metadata: {
|
||||||
})
|
isDebtCollection: debtInfo.isDebtCollection,
|
||||||
.select()
|
subject: data.Subject,
|
||||||
.single();
|
fromEmail: fromEmail,
|
||||||
|
toEmail: toEmail,
|
||||||
|
},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!insertError && insertedDebt) {
|
|
||||||
// Record the initial debt email as the first conversation message
|
// Record the initial debt email as the first conversation message
|
||||||
await supabaseAdmin.from("conversation_messages").insert({
|
await appwriteAdmin.databases.createDocument(
|
||||||
debt_id: insertedDebt.id,
|
DATABASE_ID,
|
||||||
message_type: "initial_debt",
|
COLLECTIONS.CONVERSATION_MESSAGES,
|
||||||
direction: "inbound",
|
ID.unique(),
|
||||||
subject: data.Subject,
|
{
|
||||||
body: textBody,
|
debt_id: insertedDebt.$id,
|
||||||
from_email: fromEmail,
|
message_type: "initial_debt",
|
||||||
to_email: toEmail,
|
direction: "inbound",
|
||||||
message_id: data.MessageID || `initial-${Date.now()}`,
|
subject: data.Subject,
|
||||||
});
|
body: textBody,
|
||||||
}
|
from_email: fromEmail,
|
||||||
|
to_email: toEmail,
|
||||||
if (insertError) {
|
message_id: data.MessageID || `initial-${Date.now()}`,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (insertError) {
|
||||||
console.error("Error inserting debt:", insertError);
|
console.error("Error inserting debt:", insertError);
|
||||||
const errorInfo = handleDatabaseError(insertError);
|
const errorInfo = handleDatabaseError(insertError);
|
||||||
|
|
||||||
@@ -472,33 +522,42 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log the email receipt
|
// Log the email receipt
|
||||||
await supabaseAdmin.from("audit_logs").insert({
|
await appwriteAdmin.databases.createDocument(
|
||||||
debt_id: insertedDebt.id,
|
DATABASE_ID,
|
||||||
action: "email_received",
|
COLLECTIONS.AUDIT_LOGS,
|
||||||
details: {
|
ID.unique(),
|
||||||
vendor: debtInfo.vendor,
|
{
|
||||||
amount: debtInfo.amount,
|
debt_id: insertedDebt.$id,
|
||||||
subject: data.Subject,
|
action: "email_received",
|
||||||
aiParsed: true,
|
details: {
|
||||||
},
|
vendor: debtInfo.vendor,
|
||||||
});
|
amount: debtInfo.amount,
|
||||||
|
subject: data.Subject,
|
||||||
|
aiParsed: true,
|
||||||
|
},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Trigger negotiation function if this is a legitimate debt
|
// Trigger negotiation function if this is a legitimate debt
|
||||||
if (debtInfo.amount > 0 && debtInfo.isDebtCollection) {
|
if (debtInfo.amount > 0 && debtInfo.isDebtCollection) {
|
||||||
// Access environment variables through Astro runtime
|
// Access environment variables through Astro runtime
|
||||||
const supabaseUrl = process.env.SUPABASE_URL ||
|
const appwriteEndpoint = process.env.PUBLIC_APPWRITE_ENDPOINT ||
|
||||||
import.meta.env.PUBLIC_SUPABASE_URL;
|
import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
|
||||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
const appwriteProjectId = process.env.PUBLIC_APPWRITE_PROJECT_ID ||
|
||||||
import.meta.env.SUPABASE_SERVICE_ROLE_KEY;
|
import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
|
||||||
|
const appwriteApiKey = process.env.APPWRITE_API_KEY ||
|
||||||
|
import.meta.env.APPWRITE_API_KEY;
|
||||||
|
|
||||||
if (supabaseUrl && supabaseServiceKey) {
|
if (appwriteEndpoint && appwriteProjectId && appwriteApiKey) {
|
||||||
const negotiateUrl = `${supabaseUrl}/functions/v1/negotiate`;
|
const negotiateUrl = `${appwriteEndpoint}/functions/v1/negotiate`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(negotiateUrl, {
|
await fetch(negotiateUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${supabaseServiceKey}`,
|
"X-Appwrite-Project": appwriteProjectId,
|
||||||
|
"X-Appwrite-Key": appwriteApiKey,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ record: insertedDebt }),
|
body: JSON.stringify({ record: insertedDebt }),
|
||||||
@@ -509,7 +568,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Supabase environment variables not configured for negotiation trigger",
|
"Appwrite environment variables not configured for negotiation trigger",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user