Merge pull request 'Prod Deployment Preparation' (#4) from dev into loaf-prod
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -156,6 +156,12 @@ cython_debug/
|
|||||||
backups/
|
backups/
|
||||||
*.backup
|
*.backup
|
||||||
|
|
||||||
|
# ===== Alembic Migrations =====
|
||||||
|
# Keep migration files but ignore bytecode
|
||||||
|
alembic/__pycache__/
|
||||||
|
alembic/versions/__pycache__/
|
||||||
|
# Keep alembic.ini, env.py, and all migration files in alembic/versions/
|
||||||
|
|
||||||
# ===== IDE / Editors =====
|
# ===== IDE / Editors =====
|
||||||
# VSCode
|
# VSCode
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
247
DATABASE_STATUS.md
Normal file
247
DATABASE_STATUS.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# Database Status - LOAF Membership Platform
|
||||||
|
|
||||||
|
**Database:** `loaf_new`
|
||||||
|
**Host:** 10.9.23.11:54321
|
||||||
|
**Last Updated:** 2026-01-03
|
||||||
|
**Status:** ✅ Fully initialized with seed data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Summary
|
||||||
|
|
||||||
|
### Tables (18 total)
|
||||||
|
|
||||||
|
| Table Name | Status | Records | Purpose |
|
||||||
|
|------------|--------|---------|---------|
|
||||||
|
| ✅ alembic_version | Active | 1 | Migration tracking (001_initial_baseline) |
|
||||||
|
| ✅ users | Active | 0 | User accounts and profiles |
|
||||||
|
| ✅ events | Active | 0 | Event management |
|
||||||
|
| ✅ event_rsvps | Active | 0 | Event RSVPs and attendance |
|
||||||
|
| ✅ event_galleries | Active | 0 | Event photo galleries |
|
||||||
|
| ✅ roles | Active | 5 | RBAC role definitions |
|
||||||
|
| ✅ permissions | Active | 25 | RBAC permission definitions |
|
||||||
|
| ✅ role_permissions | Active | 49 | Role-permission mappings |
|
||||||
|
| ✅ user_invitations | Active | 0 | Admin invitation system |
|
||||||
|
| ✅ subscriptions | Active | 0 | User subscriptions |
|
||||||
|
| ✅ subscription_plans | Active | 3 | Available membership plans |
|
||||||
|
| ✅ donations | Active | 0 | Donation tracking |
|
||||||
|
| ✅ import_jobs | Active | 0 | CSV import tracking |
|
||||||
|
| ✅ import_rollback_audit | Active | 0 | Import rollback audit trail |
|
||||||
|
| ✅ newsletter_archives | Active | 0 | Newsletter document archive |
|
||||||
|
| ✅ financial_reports | Active | 0 | Financial document archive |
|
||||||
|
| ✅ bylaws_documents | Active | 0 | Bylaws document archive |
|
||||||
|
| ✅ storage_usage | Active | 1 | Storage quota tracking (100GB limit) |
|
||||||
|
|
||||||
|
### ENUMs (8 total)
|
||||||
|
|
||||||
|
| ENUM Name | Values | Used By |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| ✅ userstatus | pending_email, awaiting_event, pre_approved, payment_pending, active, inactive | users.status |
|
||||||
|
| ✅ userrole | guest, member, admin, finance, superadmin | users.role, user_invitations.role |
|
||||||
|
| ✅ rsvpstatus | yes, no, maybe | event_rsvps.rsvp_status |
|
||||||
|
| ✅ subscriptionstatus | active, past_due, canceled, incomplete, trialing | subscriptions.status |
|
||||||
|
| ✅ donationtype | one_time, recurring, pledge, in_kind, memorial | donations.donation_type |
|
||||||
|
| ✅ donationstatus | pending, completed, failed, refunded | donations.status |
|
||||||
|
| ✅ invitationstatus | pending, accepted, expired, revoked | user_invitations.status |
|
||||||
|
| ✅ importjobstatus | processing, completed, failed | import_jobs.status |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seed Data Loaded
|
||||||
|
|
||||||
|
### Roles (5)
|
||||||
|
|
||||||
|
| Code | Name | System Role | Permissions |
|
||||||
|
|------|------|-------------|-------------|
|
||||||
|
| admin | Admin | Yes | 16 |
|
||||||
|
| finance | Finance | Yes | 7 |
|
||||||
|
| guest | Guest | Yes | 0 |
|
||||||
|
| member | Member | Yes | 1 |
|
||||||
|
| superadmin | Super Admin | Yes | 25 |
|
||||||
|
|
||||||
|
### Permissions (25 across 5 modules)
|
||||||
|
|
||||||
|
**Users Module (6 permissions):**
|
||||||
|
- users.view - View Users
|
||||||
|
- users.create - Create Users
|
||||||
|
- users.edit - Edit Users
|
||||||
|
- users.delete - Delete Users
|
||||||
|
- users.approve - Approve Users
|
||||||
|
- users.import - Import Users
|
||||||
|
|
||||||
|
**Events Module (6 permissions):**
|
||||||
|
- events.view - View Events
|
||||||
|
- events.create - Create Events
|
||||||
|
- events.edit - Edit Events
|
||||||
|
- events.delete - Delete Events
|
||||||
|
- events.publish - Publish Events
|
||||||
|
- events.manage_attendance - Manage Attendance
|
||||||
|
|
||||||
|
**Finance Module (5 permissions):**
|
||||||
|
- finance.view - View Financial Data
|
||||||
|
- finance.manage_plans - Manage Subscription Plans
|
||||||
|
- finance.manage_subscriptions - Manage Subscriptions
|
||||||
|
- finance.view_reports - View Financial Reports
|
||||||
|
- finance.export - Export Financial Data
|
||||||
|
|
||||||
|
**Content Module (3 permissions):**
|
||||||
|
- content.newsletters - Manage Newsletters
|
||||||
|
- content.documents - Manage Documents
|
||||||
|
- content.gallery - Manage Gallery
|
||||||
|
|
||||||
|
**System Module (5 permissions):**
|
||||||
|
- system.settings - System Settings
|
||||||
|
- system.roles - Manage Roles
|
||||||
|
- system.invitations - Manage Invitations
|
||||||
|
- system.storage - Manage Storage
|
||||||
|
- system.audit - View Audit Logs
|
||||||
|
|
||||||
|
### Subscription Plans (3)
|
||||||
|
|
||||||
|
| Plan Name | Price | Billing | Custom Pricing | Donation Support |
|
||||||
|
|-----------|-------|---------|----------------|------------------|
|
||||||
|
| Pay What You Want Membership | $30.00 (min) | Annual | ✅ Yes | ✅ Yes |
|
||||||
|
| Annual Individual Membership | $60.00 | Annual | ❌ No | ❌ No |
|
||||||
|
| Annual Group Membership | $100.00 | Annual | ❌ No | ❌ No |
|
||||||
|
|
||||||
|
**Note:** Stripe price IDs need to be configured after Stripe setup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Status
|
||||||
|
|
||||||
|
**Current Revision:** `001_initial_baseline (head)`
|
||||||
|
**Migration System:** Alembic 1.14.0
|
||||||
|
**Schema Source:** `migrations/000_initial_schema.sql`
|
||||||
|
**Seed Source:** `migrations/seed_data.sql`
|
||||||
|
|
||||||
|
**Migration History:**
|
||||||
|
- `001_initial_baseline` - Empty baseline marker (2026-01-02)
|
||||||
|
|
||||||
|
**Future migrations** will be generated using:
|
||||||
|
```bash
|
||||||
|
alembic revision --autogenerate -m "description"
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Immediate (Required)
|
||||||
|
|
||||||
|
1. **Create Superadmin User**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python3 create_superadmin.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Stripe Price IDs**
|
||||||
|
```sql
|
||||||
|
UPDATE subscription_plans
|
||||||
|
SET stripe_price_id = 'price_xxx'
|
||||||
|
WHERE name = 'Annual Individual Membership';
|
||||||
|
|
||||||
|
UPDATE subscription_plans
|
||||||
|
SET stripe_price_id = 'price_yyy'
|
||||||
|
WHERE name = 'Annual Group Membership';
|
||||||
|
|
||||||
|
UPDATE subscription_plans
|
||||||
|
SET stripe_price_id = 'price_zzz'
|
||||||
|
WHERE name = 'Pay What You Want Membership';
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set Environment Variables**
|
||||||
|
- Copy `backend/.env.example` to `backend/.env`
|
||||||
|
- Fill in all required values (DATABASE_URL, JWT_SECRET, SMTP, Stripe, R2)
|
||||||
|
|
||||||
|
4. **Test Application**
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
uvicorn server:app --reload
|
||||||
|
|
||||||
|
# Frontend (separate terminal)
|
||||||
|
cd frontend
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional (Recommended)
|
||||||
|
|
||||||
|
1. **Add Sample Events**
|
||||||
|
- Login as superadmin
|
||||||
|
- Navigate to Admin → Events
|
||||||
|
- Create 2-3 sample events
|
||||||
|
|
||||||
|
2. **Test Registration Flow**
|
||||||
|
- Register a test user
|
||||||
|
- Verify email verification works
|
||||||
|
- Test event RSVP
|
||||||
|
- Test admin approval flow
|
||||||
|
|
||||||
|
3. **Configure Email Templates**
|
||||||
|
- Review templates in `backend/email_service.py`
|
||||||
|
- Customize colors, branding, copy
|
||||||
|
|
||||||
|
4. **Set Up Monitoring**
|
||||||
|
- Configure error logging
|
||||||
|
- Set up uptime monitoring
|
||||||
|
- Configure backup schedule
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Maintenance
|
||||||
|
|
||||||
|
### Backup Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PGPASSWORD='your-password' pg_dump -h 10.9.23.11 -p 54321 -U postgres loaf_new > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PGPASSWORD='your-password' psql -h 10.9.23.11 -p 54321 -U postgres -d loaf_new < backup_file.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check Queries
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Check user count by status
|
||||||
|
SELECT status, COUNT(*) FROM users GROUP BY status;
|
||||||
|
|
||||||
|
-- Check upcoming events
|
||||||
|
SELECT title, start_at FROM events WHERE start_at > NOW() ORDER BY start_at LIMIT 5;
|
||||||
|
|
||||||
|
-- Check active subscriptions
|
||||||
|
SELECT COUNT(*) FROM subscriptions WHERE status = 'active';
|
||||||
|
|
||||||
|
-- Check storage usage
|
||||||
|
SELECT
|
||||||
|
total_bytes_used / 1024 / 1024 / 1024 as used_gb,
|
||||||
|
max_bytes_allowed / 1024 / 1024 / 1024 as max_gb,
|
||||||
|
ROUND((total_bytes_used::numeric / max_bytes_allowed * 100)::numeric, 2) as percent_used
|
||||||
|
FROM storage_usage;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support & Resources
|
||||||
|
|
||||||
|
- **Deployment Guide:** See `DEPLOYMENT.md` for complete deployment instructions
|
||||||
|
- **API Documentation:** http://localhost:8000/docs (when backend running)
|
||||||
|
- **Alembic Guide:** See `backend/alembic/README.md` for migration documentation
|
||||||
|
- **Project Documentation:** See `CLAUDE.md` for codebase overview
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
**2026-01-03:**
|
||||||
|
- ✅ Created all 17 data tables
|
||||||
|
- ✅ Created all 8 ENUMs
|
||||||
|
- ✅ Loaded seed data (5 roles, 25 permissions, 3 subscription plans)
|
||||||
|
- ✅ Initialized Alembic tracking (001_initial_baseline)
|
||||||
|
- ✅ Created superadmin user helper script
|
||||||
|
|
||||||
|
**Status:** Database is fully initialized and ready for use. Next step: Create superadmin user and start application.
|
||||||
379
DEPLOYMENT.md
Normal file
379
DEPLOYMENT.md
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
# Deployment Guide - LOAF Membership Platform
|
||||||
|
|
||||||
|
## Fresh Database Installation
|
||||||
|
|
||||||
|
Follow these steps in order for a **brand new deployment**:
|
||||||
|
|
||||||
|
### Step 1: Create PostgreSQL Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect to PostgreSQL
|
||||||
|
psql -U postgres
|
||||||
|
|
||||||
|
# Create database
|
||||||
|
CREATE DATABASE membership_db;
|
||||||
|
|
||||||
|
# Create user (if needed)
|
||||||
|
CREATE USER loaf_admin WITH PASSWORD 'your-secure-password';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE membership_db TO loaf_admin;
|
||||||
|
|
||||||
|
# Exit PostgreSQL
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Run Initial Schema
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# Apply the complete schema (creates all 17 tables, 8 enums, indexes)
|
||||||
|
psql -U loaf_admin -d membership_db -f migrations/000_initial_schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**What this creates:**
|
||||||
|
- ✅ 17 tables: users, events, subscriptions, roles, permissions, etc.
|
||||||
|
- ✅ 8 custom enums: UserStatus, UserRole, RSVPStatus, etc.
|
||||||
|
- ✅ All indexes and foreign keys
|
||||||
|
- ✅ All constraints and defaults
|
||||||
|
|
||||||
|
### Step 3: Mark Database for Alembic Tracking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mark the database as being at the baseline
|
||||||
|
alembic stamp head
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Verify Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Alembic status
|
||||||
|
alembic current
|
||||||
|
# Expected output: 001_initial_baseline (head)
|
||||||
|
|
||||||
|
# Check database tables
|
||||||
|
psql -U loaf_admin -d membership_db -c "\dt"
|
||||||
|
# Should show 17 tables
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Set Environment Variables
|
||||||
|
|
||||||
|
Create `backend/.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://loaf_admin:your-password@localhost:5432/membership_db
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=your-secret-key-minimum-32-characters-long
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
|
||||||
|
# Email (SMTP)
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=your-email@gmail.com
|
||||||
|
SMTP_PASSWORD=your-app-password
|
||||||
|
SMTP_FROM_EMAIL=noreply@loafmembers.org
|
||||||
|
SMTP_FROM_NAME=LOAF Membership
|
||||||
|
|
||||||
|
# Frontend URL
|
||||||
|
FRONTEND_URL=https://members.loafmembers.org
|
||||||
|
|
||||||
|
# Cloudflare R2
|
||||||
|
R2_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com
|
||||||
|
R2_ACCESS_KEY_ID=your-r2-access-key
|
||||||
|
R2_SECRET_ACCESS_KEY=your-r2-secret-key
|
||||||
|
R2_BUCKET_NAME=loaf-membership
|
||||||
|
R2_PUBLIC_URL=https://cdn.loafmembers.org
|
||||||
|
|
||||||
|
# Stripe
|
||||||
|
STRIPE_SECRET_KEY=sk_live_...
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||||
|
STRIPE_PRICE_ID_ANNUAL=price_...
|
||||||
|
STRIPE_PRICE_ID_GROUP=price_...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd ../frontend
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Start Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend (in backend/)
|
||||||
|
uvicorn server:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
# Frontend (in frontend/)
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Create First Superadmin User
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect to database
|
||||||
|
psql -U loaf_admin -d membership_db
|
||||||
|
|
||||||
|
# Create superadmin user
|
||||||
|
INSERT INTO users (
|
||||||
|
id, email, password_hash, first_name, last_name,
|
||||||
|
status, role, email_verified, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
gen_random_uuid(),
|
||||||
|
'admin@loafmembers.org',
|
||||||
|
'$2b$12$your-bcrypt-hashed-password-here', -- Use bcrypt to hash password
|
||||||
|
'Admin',
|
||||||
|
'User',
|
||||||
|
'active',
|
||||||
|
'superadmin',
|
||||||
|
true,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generate password hash:**
|
||||||
|
```python
|
||||||
|
import bcrypt
|
||||||
|
password = b"your-secure-password"
|
||||||
|
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
|
||||||
|
print(hashed.decode())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Existing Database Update
|
||||||
|
|
||||||
|
For **updating an existing deployment** with new code:
|
||||||
|
|
||||||
|
### Step 1: Backup Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pg_dump -U loaf_admin membership_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Pull Latest Code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Install New Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd ../frontend
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Apply Database Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# Check current migration status
|
||||||
|
alembic current
|
||||||
|
|
||||||
|
# Apply pending migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
alembic current
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Restart Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart backend
|
||||||
|
systemctl restart membership-backend
|
||||||
|
|
||||||
|
# Rebuild and restart frontend
|
||||||
|
cd frontend
|
||||||
|
yarn build
|
||||||
|
systemctl restart membership-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## First-Time Alembic Setup (Existing Database)
|
||||||
|
|
||||||
|
If you have an **existing database** that was created with `000_initial_schema.sql` but hasn't been marked for Alembic tracking:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# Mark database as being at the baseline (one-time only)
|
||||||
|
alembic stamp head
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
alembic current
|
||||||
|
# Expected output: 001_initial_baseline (head)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema Verification
|
||||||
|
|
||||||
|
**Check all tables exist:**
|
||||||
|
```bash
|
||||||
|
psql -U loaf_admin -d membership_db -c "\dt"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected tables (17 total):**
|
||||||
|
- users
|
||||||
|
- events
|
||||||
|
- event_rsvps
|
||||||
|
- subscriptions
|
||||||
|
- subscription_plans
|
||||||
|
- permissions
|
||||||
|
- roles
|
||||||
|
- role_permissions
|
||||||
|
- user_invitations
|
||||||
|
- import_jobs
|
||||||
|
- import_rollback_audit
|
||||||
|
- event_galleries
|
||||||
|
- newsletter_archives
|
||||||
|
- financial_reports
|
||||||
|
- bylaws_documents
|
||||||
|
- donations
|
||||||
|
- storage_usage
|
||||||
|
|
||||||
|
**Check enums:**
|
||||||
|
```bash
|
||||||
|
psql -U loaf_admin -d membership_db -c "\dT"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected enums (8 total):**
|
||||||
|
- userstatus
|
||||||
|
- userrole
|
||||||
|
- rsvpstatus
|
||||||
|
- subscriptionstatus
|
||||||
|
- donationtype
|
||||||
|
- donationstatus
|
||||||
|
- invitationstatus
|
||||||
|
- importjobstatus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Procedures
|
||||||
|
|
||||||
|
### Rollback Last Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
alembic downgrade -1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback to Specific Revision
|
||||||
|
|
||||||
|
```bash
|
||||||
|
alembic downgrade <revision_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Database Reset
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WARNING: This deletes ALL data!
|
||||||
|
|
||||||
|
# 1. Backup first
|
||||||
|
pg_dump -U loaf_admin membership_db > emergency_backup.sql
|
||||||
|
|
||||||
|
# 2. Drop database
|
||||||
|
dropdb membership_db
|
||||||
|
|
||||||
|
# 3. Recreate database
|
||||||
|
createdb membership_db
|
||||||
|
|
||||||
|
# 4. Run initial schema
|
||||||
|
psql -U loaf_admin -d membership_db -f backend/migrations/000_initial_schema.sql
|
||||||
|
|
||||||
|
# 5. Mark for Alembic
|
||||||
|
cd backend
|
||||||
|
alembic stamp head
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "relation does not exist" error
|
||||||
|
|
||||||
|
The database wasn't initialized properly.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
psql -U loaf_admin -d membership_db -f backend/migrations/000_initial_schema.sql
|
||||||
|
alembic stamp head
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Target database is not up to date"
|
||||||
|
|
||||||
|
Migrations haven't been applied.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Can't locate revision"
|
||||||
|
|
||||||
|
Alembic tracking is out of sync.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Check what revision the database thinks it's at
|
||||||
|
alembic current
|
||||||
|
|
||||||
|
# If empty or wrong, manually set it
|
||||||
|
alembic stamp head
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database connection errors
|
||||||
|
|
||||||
|
Check `.env` file has correct `DATABASE_URL`.
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
```
|
||||||
|
DATABASE_URL=postgresql://username:password@host:port/database
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
Before going live:
|
||||||
|
|
||||||
|
- [ ] Database created and schema applied
|
||||||
|
- [ ] Alembic marked as up-to-date (`alembic current` shows baseline)
|
||||||
|
- [ ] All environment variables set in `.env`
|
||||||
|
- [ ] Dependencies installed (Python + Node)
|
||||||
|
- [ ] Superadmin user created
|
||||||
|
- [ ] SSL certificates configured
|
||||||
|
- [ ] Backup system in place
|
||||||
|
- [ ] Monitoring configured
|
||||||
|
- [ ] Domain DNS pointing to server
|
||||||
|
- [ ] Email sending verified (SMTP working)
|
||||||
|
- [ ] Stripe webhook endpoint configured
|
||||||
|
- [ ] R2 bucket accessible and CORS configured
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues:
|
||||||
|
1. Check logs: `tail -f backend/logs/app.log`
|
||||||
|
2. Check Alembic status: `alembic current`
|
||||||
|
3. Verify environment variables: `cat backend/.env`
|
||||||
|
4. Test database connection: `psql -U loaf_admin -d membership_db`
|
||||||
118
alembic.ini
Normal file
118
alembic.ini
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
# Use forward slashes (/) also on windows to provide an os agnostic path
|
||||||
|
script_location = alembic
|
||||||
|
|
||||||
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||||
|
# Uncomment the line below if you want the files to be prepended with date and time
|
||||||
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||||
|
# for all available tokens
|
||||||
|
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory.
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
# timezone to use when rendering the date within the migration file
|
||||||
|
# as well as the filename.
|
||||||
|
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
||||||
|
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
||||||
|
# string value is passed to ZoneInfo()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the "slug" field
|
||||||
|
# truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; This defaults
|
||||||
|
# to alembic/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path.
|
||||||
|
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||||
|
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
||||||
|
|
||||||
|
# version path separator; As mentioned above, this is the character used to split
|
||||||
|
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||||
|
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||||
|
# Valid values for version_path_separator are:
|
||||||
|
#
|
||||||
|
# version_path_separator = :
|
||||||
|
# version_path_separator = ;
|
||||||
|
# version_path_separator = space
|
||||||
|
# version_path_separator = newline
|
||||||
|
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||||
|
|
||||||
|
# set to 'true' to search source files recursively
|
||||||
|
# in each "version_locations" directory
|
||||||
|
# new in Alembic version 1.10
|
||||||
|
# recursive_version_locations = false
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
# sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
# Database URL is configured in alembic/env.py from .env file for security
|
||||||
|
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
# on newly generated revision scripts. See the documentation for further
|
||||||
|
# detail and examples
|
||||||
|
|
||||||
|
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||||
|
# hooks = black
|
||||||
|
# black.type = console_scripts
|
||||||
|
# black.entrypoint = black
|
||||||
|
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||||
|
# hooks = ruff
|
||||||
|
# ruff.type = exec
|
||||||
|
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||||
|
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARNING
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARNING
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
1
alembic/README
Normal file
1
alembic/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
259
alembic/README.md
Normal file
259
alembic/README.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# Alembic Database Migrations
|
||||||
|
|
||||||
|
This directory contains **Alembic** database migrations for the LOAF membership platform.
|
||||||
|
|
||||||
|
## What is Alembic?
|
||||||
|
|
||||||
|
Alembic is a lightweight database migration tool for SQLAlchemy. It allows you to:
|
||||||
|
- Track database schema changes over time
|
||||||
|
- Apply migrations incrementally
|
||||||
|
- Roll back changes if needed
|
||||||
|
- Auto-generate migration scripts from model changes
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
alembic/
|
||||||
|
├── versions/ # Migration scripts (KEEP IN VERSION CONTROL)
|
||||||
|
│ └── *.py # Individual migration files
|
||||||
|
├── env.py # Alembic environment configuration
|
||||||
|
├── script.py.mako # Template for new migration files
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Create a New Migration
|
||||||
|
|
||||||
|
After making changes to `models.py`, generate a migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
alembic revision --autogenerate -m "add_user_bio_field"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a new file in `alembic/versions/` like:
|
||||||
|
```
|
||||||
|
3e02c74581c9_add_user_bio_field.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Review the Generated Migration
|
||||||
|
|
||||||
|
**IMPORTANT:** Always review auto-generated migrations before applying them!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open the latest migration file
|
||||||
|
cat alembic/versions/3e02c74581c9_add_user_bio_field.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Check:
|
||||||
|
- ✅ The `upgrade()` function contains the correct changes
|
||||||
|
- ✅ The `downgrade()` function properly reverses those changes
|
||||||
|
- ✅ No unintended table drops or data loss
|
||||||
|
|
||||||
|
### 3. Apply the Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply all pending migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Or apply migrations one at a time
|
||||||
|
alembic upgrade +1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Rollback a Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rollback the last migration
|
||||||
|
alembic downgrade -1
|
||||||
|
|
||||||
|
# Rollback to a specific revision
|
||||||
|
alembic downgrade 3e02c74581c9
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `alembic current` | Show current migration revision |
|
||||||
|
| `alembic history` | Show migration history |
|
||||||
|
| `alembic heads` | Show head revisions |
|
||||||
|
| `alembic upgrade head` | Apply all pending migrations |
|
||||||
|
| `alembic downgrade -1` | Rollback last migration |
|
||||||
|
| `alembic revision --autogenerate -m "message"` | Create new migration |
|
||||||
|
| `alembic stamp head` | Mark database as up-to-date without running migrations |
|
||||||
|
|
||||||
|
## Migration Workflow
|
||||||
|
|
||||||
|
### For Development
|
||||||
|
|
||||||
|
1. **Make changes to `models.py`**
|
||||||
|
```python
|
||||||
|
# In models.py
|
||||||
|
class User(Base):
|
||||||
|
# ...existing fields...
|
||||||
|
bio = Column(Text, nullable=True) # New field
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Generate migration**
|
||||||
|
```bash
|
||||||
|
alembic revision --autogenerate -m "add_user_bio_field"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Review the generated file**
|
||||||
|
```python
|
||||||
|
# In alembic/versions/xxxxx_add_user_bio_field.py
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('users', sa.Column('bio', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('users', 'bio')
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Apply migration**
|
||||||
|
```bash
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Commit migration file to Git**
|
||||||
|
```bash
|
||||||
|
git add alembic/versions/xxxxx_add_user_bio_field.py
|
||||||
|
git commit -m "Add user bio field"
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Production Deployment
|
||||||
|
|
||||||
|
**Fresh Database (New Installation):**
|
||||||
|
```bash
|
||||||
|
# 1. Create database
|
||||||
|
createdb membership_db
|
||||||
|
|
||||||
|
# 2. Run initial schema SQL (creates all 17 tables)
|
||||||
|
psql -U username -d membership_db -f ../migrations/000_initial_schema.sql
|
||||||
|
|
||||||
|
# 3. Mark database as up-to-date with Alembic
|
||||||
|
alembic stamp head
|
||||||
|
|
||||||
|
# 4. Verify
|
||||||
|
alembic current # Should show: 001_initial_baseline (head)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Existing Database (Apply New Migrations):**
|
||||||
|
```bash
|
||||||
|
# 1. Pull latest code
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 2. Apply migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# 3. Verify
|
||||||
|
alembic current
|
||||||
|
|
||||||
|
# 4. Restart application
|
||||||
|
systemctl restart membership-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Database Connection
|
||||||
|
|
||||||
|
Alembic reads the `DATABASE_URL` from your `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/membership_db
|
||||||
|
```
|
||||||
|
|
||||||
|
The connection is configured in `alembic/env.py` (lines 29-36).
|
||||||
|
|
||||||
|
### Target Metadata
|
||||||
|
|
||||||
|
Alembic uses `Base.metadata` from `models.py` to detect changes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In alembic/env.py
|
||||||
|
from models import Base
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
### ✅ DO:
|
||||||
|
- Always review auto-generated migrations before applying
|
||||||
|
- Test migrations in development before production
|
||||||
|
- Commit migration files to version control
|
||||||
|
- Write descriptive migration messages
|
||||||
|
- Include both `upgrade()` and `downgrade()` functions
|
||||||
|
|
||||||
|
### ❌ DON'T:
|
||||||
|
- Don't edit migration files after they've been applied in production
|
||||||
|
- Don't delete migration files from `alembic/versions/`
|
||||||
|
- Don't modify the `revision` or `down_revision` values
|
||||||
|
- Don't commit `.pyc` files (already in .gitignore)
|
||||||
|
|
||||||
|
## Migration History
|
||||||
|
|
||||||
|
| Revision | Description | Date | Type |
|
||||||
|
|----------|-------------|------|------|
|
||||||
|
| `001_initial_baseline` | Baseline marker (empty migration) | 2026-01-02 | Baseline |
|
||||||
|
|
||||||
|
**Note:** The actual initial schema is created by running `backend/migrations/000_initial_schema.sql`. The baseline migration is an empty marker that indicates the starting point for Alembic tracking.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Target database is not up to date"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current revision
|
||||||
|
alembic current
|
||||||
|
|
||||||
|
# Check pending migrations
|
||||||
|
alembic history
|
||||||
|
|
||||||
|
# Apply missing migrations
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
### "FAILED: Can't locate revision identified by 'xxxxx'"
|
||||||
|
|
||||||
|
The database thinks it's at a revision that doesn't exist in your `alembic/versions/`.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Mark database at a known good revision
|
||||||
|
alembic stamp head
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration conflicts
|
||||||
|
|
||||||
|
If you get merge conflicts in migration files:
|
||||||
|
|
||||||
|
1. Resolve conflicts in the migration file
|
||||||
|
2. Ensure `revision` and `down_revision` chain is correct
|
||||||
|
3. Test the migration locally
|
||||||
|
|
||||||
|
### Fresh database setup
|
||||||
|
|
||||||
|
For a completely new database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Run initial schema SQL
|
||||||
|
psql -U username -d membership_db -f ../migrations/000_initial_schema.sql
|
||||||
|
|
||||||
|
# Step 2: Mark as up-to-date
|
||||||
|
alembic stamp head
|
||||||
|
|
||||||
|
# Step 3: Verify
|
||||||
|
alembic current # Should show: 001_initial_baseline (head)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Legacy Migrations
|
||||||
|
|
||||||
|
Old numbered SQL migrations (`000_initial_schema.sql` through `011_wordpress_import_enhancements.sql`) are preserved in `backend/migrations/` for reference. These have been consolidated into the initial Alembic migration.
|
||||||
|
|
||||||
|
**Going forward, all new migrations must use Alembic.**
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Alembic Documentation](https://alembic.sqlalchemy.org/)
|
||||||
|
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)
|
||||||
|
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
|
||||||
96
alembic/env.py
Normal file
96
alembic/env.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
from alembic import context
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Add the parent directory to the path so we can import our models
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Import all models so Alembic can detect them
|
||||||
|
from models import Base
|
||||||
|
import models # This ensures all models are imported
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# Set the SQLAlchemy URL from environment variable
|
||||||
|
database_url = os.getenv("DATABASE_URL")
|
||||||
|
if database_url:
|
||||||
|
config.set_main_option("sqlalchemy.url", database_url)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"DATABASE_URL environment variable not set. "
|
||||||
|
"Please create a .env file with DATABASE_URL=postgresql://user:password@host:port/dbname"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add your model's MetaData object here for 'autogenerate' support
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
compare_type=True, # Detect type changes
|
||||||
|
compare_server_default=True, # Detect default value changes
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section, {}),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True, # Detect type changes
|
||||||
|
compare_server_default=True, # Detect default value changes
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
26
alembic/script.py.mako
Normal file
26
alembic/script.py.mako
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = ${repr(up_revision)}
|
||||||
|
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||||
|
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
59
alembic/versions/001_initial_baseline.py
Normal file
59
alembic/versions/001_initial_baseline.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""initial_baseline - Use 000_initial_schema.sql for fresh deployments
|
||||||
|
|
||||||
|
Revision ID: 001_initial_baseline
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-01-02 16:45:00.000000
|
||||||
|
|
||||||
|
IMPORTANT: This is a baseline migration for existing databases.
|
||||||
|
|
||||||
|
For FRESH deployments:
|
||||||
|
1. Run: psql -U user -d dbname -f backend/migrations/000_initial_schema.sql
|
||||||
|
2. Run: alembic stamp head
|
||||||
|
|
||||||
|
For EXISTING deployments (already have database):
|
||||||
|
1. Run: alembic stamp head (marks database as up-to-date)
|
||||||
|
|
||||||
|
This migration intentionally does NOTHING because:
|
||||||
|
- Fresh deployments use 000_initial_schema.sql to create all tables
|
||||||
|
- Existing deployments already have all tables from 000_initial_schema.sql
|
||||||
|
- Future migrations will be incremental changes from this baseline
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '001_initial_baseline'
|
||||||
|
down_revision: Union[str, None] = None
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""
|
||||||
|
This migration does nothing.
|
||||||
|
|
||||||
|
It serves as a baseline marker that indicates:
|
||||||
|
- All 17 tables exist (users, events, subscriptions, etc.)
|
||||||
|
- All 8 enums are defined (UserStatus, UserRole, etc.)
|
||||||
|
- All indexes and constraints are in place
|
||||||
|
|
||||||
|
The actual schema is created by running:
|
||||||
|
backend/migrations/000_initial_schema.sql
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""
|
||||||
|
Cannot downgrade below baseline.
|
||||||
|
|
||||||
|
If you need to completely reset the database:
|
||||||
|
1. dropdb dbname
|
||||||
|
2. createdb dbname
|
||||||
|
3. psql -U user -d dbname -f backend/migrations/000_initial_schema.sql
|
||||||
|
4. alembic stamp head
|
||||||
|
"""
|
||||||
|
pass
|
||||||
105
create_superadmin.py
Normal file
105
create_superadmin.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Create Superadmin User Script
|
||||||
|
Generates a superadmin user with hashed password for LOAF membership platform
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from getpass import getpass
|
||||||
|
|
||||||
|
def generate_password_hash(password: str) -> str:
|
||||||
|
"""Generate bcrypt hash for password"""
|
||||||
|
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||||
|
|
||||||
|
def generate_sql(email: str, password_hash: str, first_name: str, last_name: str) -> str:
|
||||||
|
"""Generate SQL INSERT statement"""
|
||||||
|
return f"""
|
||||||
|
-- Create Superadmin User
|
||||||
|
INSERT INTO users (
|
||||||
|
id, email, password_hash, first_name, last_name,
|
||||||
|
status, role, email_verified, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
gen_random_uuid(),
|
||||||
|
'{email}',
|
||||||
|
'{password_hash}',
|
||||||
|
'{first_name}',
|
||||||
|
'{last_name}',
|
||||||
|
'active',
|
||||||
|
'superadmin',
|
||||||
|
true,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("LOAF Membership Platform - Superadmin User Creator")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Get user input
|
||||||
|
email = input("Email address: ").strip()
|
||||||
|
if not email or '@' not in email:
|
||||||
|
print("❌ Invalid email address")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
first_name = input("First name: ").strip()
|
||||||
|
if not first_name:
|
||||||
|
print("❌ First name is required")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
last_name = input("Last name: ").strip()
|
||||||
|
if not last_name:
|
||||||
|
print("❌ Last name is required")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Get password securely
|
||||||
|
password = getpass("Password: ")
|
||||||
|
if len(password) < 8:
|
||||||
|
print("❌ Password must be at least 8 characters")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
password_confirm = getpass("Confirm password: ")
|
||||||
|
if password != password_confirm:
|
||||||
|
print("❌ Passwords do not match")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Generating password hash...")
|
||||||
|
password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
print("✅ Password hash generated")
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print("SQL STATEMENT")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
sql = generate_sql(email, password_hash, first_name, last_name)
|
||||||
|
print(sql)
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
output_file = "create_superadmin.sql"
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
f.write(sql)
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"✅ SQL saved to: {output_file}")
|
||||||
|
print()
|
||||||
|
print("Run this command to create the user:")
|
||||||
|
print(f" psql -U postgres -d loaf_new -f {output_file}")
|
||||||
|
print()
|
||||||
|
print("Or copy the SQL above and run it directly in psql")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n❌ Cancelled by user")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
@@ -146,14 +146,19 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
|
|
||||||
-- Membership
|
-- Membership
|
||||||
member_since DATE,
|
member_since DATE,
|
||||||
tos_accepted BOOLEAN DEFAULT FALSE,
|
accepts_tos BOOLEAN DEFAULT FALSE,
|
||||||
tos_accepted_at TIMESTAMP WITH TIME ZONE,
|
tos_accepted_at TIMESTAMP WITH TIME ZONE,
|
||||||
newsletter_subscribed BOOLEAN DEFAULT TRUE,
|
newsletter_subscribed BOOLEAN DEFAULT TRUE,
|
||||||
|
|
||||||
-- Reminder Tracking
|
-- Reminder Tracking (from migration 004)
|
||||||
reminder_30_days_sent BOOLEAN DEFAULT FALSE,
|
email_verification_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
reminder_60_days_sent BOOLEAN DEFAULT FALSE,
|
last_email_verification_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
reminder_85_days_sent BOOLEAN DEFAULT FALSE,
|
event_attendance_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
last_event_attendance_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
payment_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
last_payment_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
renewal_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
last_renewal_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
-- WordPress Import Tracking
|
-- WordPress Import Tracking
|
||||||
import_source VARCHAR(50),
|
import_source VARCHAR(50),
|
||||||
|
|||||||
394
migrations/create_tables_only.sql
Normal file
394
migrations/create_tables_only.sql
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Create Tables Only (ENUMs already exist)
|
||||||
|
-- Run this when ENUMs exist but tables don't
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 1: Core Tables
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
first_name VARCHAR(100) NOT NULL,
|
||||||
|
last_name VARCHAR(100) NOT NULL,
|
||||||
|
phone VARCHAR(20),
|
||||||
|
address TEXT,
|
||||||
|
city VARCHAR(100),
|
||||||
|
state VARCHAR(2),
|
||||||
|
zipcode VARCHAR(10),
|
||||||
|
date_of_birth DATE,
|
||||||
|
|
||||||
|
-- Profile
|
||||||
|
profile_image_url TEXT,
|
||||||
|
bio TEXT,
|
||||||
|
interests TEXT,
|
||||||
|
|
||||||
|
-- Partner Information
|
||||||
|
partner_first_name VARCHAR(100),
|
||||||
|
partner_last_name VARCHAR(100),
|
||||||
|
partner_is_member BOOLEAN DEFAULT FALSE,
|
||||||
|
partner_plan_to_become_member BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Referral
|
||||||
|
referred_by_member_name VARCHAR(200),
|
||||||
|
|
||||||
|
-- Newsletter Preferences
|
||||||
|
newsletter_subscribed BOOLEAN DEFAULT TRUE,
|
||||||
|
newsletter_publish_name BOOLEAN DEFAULT FALSE,
|
||||||
|
newsletter_publish_photo BOOLEAN DEFAULT FALSE,
|
||||||
|
newsletter_publish_birthday BOOLEAN DEFAULT FALSE,
|
||||||
|
newsletter_publish_none BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Volunteer & Scholarship
|
||||||
|
volunteer_interests TEXT,
|
||||||
|
scholarship_requested BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Directory
|
||||||
|
show_in_directory BOOLEAN DEFAULT TRUE,
|
||||||
|
|
||||||
|
-- Lead Sources (JSON array)
|
||||||
|
lead_sources JSONB DEFAULT '[]'::jsonb,
|
||||||
|
|
||||||
|
-- Status & Role
|
||||||
|
status userstatus DEFAULT 'pending_email' NOT NULL,
|
||||||
|
role userrole DEFAULT 'guest' NOT NULL,
|
||||||
|
role_id UUID,
|
||||||
|
|
||||||
|
-- Rejection Tracking
|
||||||
|
rejection_reason TEXT,
|
||||||
|
rejected_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
rejected_by UUID REFERENCES users(id),
|
||||||
|
|
||||||
|
-- Membership
|
||||||
|
member_since DATE,
|
||||||
|
accepts_tos BOOLEAN DEFAULT FALSE,
|
||||||
|
tos_accepted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- Reminder Tracking (from migration 004)
|
||||||
|
email_verification_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
last_email_verification_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
event_attendance_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
last_event_attendance_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
payment_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
last_payment_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
renewal_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
last_renewal_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- WordPress Import Tracking
|
||||||
|
import_source VARCHAR(50),
|
||||||
|
import_job_id UUID,
|
||||||
|
wordpress_user_id BIGINT,
|
||||||
|
wordpress_registered_date TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- Authentication
|
||||||
|
email_verified BOOLEAN DEFAULT FALSE,
|
||||||
|
email_verification_token VARCHAR(255),
|
||||||
|
email_verification_expires TIMESTAMP WITH TIME ZONE,
|
||||||
|
password_reset_token VARCHAR(255),
|
||||||
|
password_reset_expires TIMESTAMP WITH TIME ZONE,
|
||||||
|
force_password_change BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Events table
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
location VARCHAR(255),
|
||||||
|
start_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
end_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
capacity INTEGER,
|
||||||
|
published BOOLEAN DEFAULT FALSE,
|
||||||
|
calendar_uid VARCHAR(255) UNIQUE,
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Event RSVPs
|
||||||
|
CREATE TABLE IF NOT EXISTS event_rsvps (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
rsvp_status rsvpstatus NOT NULL,
|
||||||
|
attended BOOLEAN DEFAULT FALSE,
|
||||||
|
attended_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE(event_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Event Gallery
|
||||||
|
CREATE TABLE IF NOT EXISTS event_galleries (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||||
|
image_url TEXT NOT NULL,
|
||||||
|
caption TEXT,
|
||||||
|
uploaded_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Import Jobs
|
||||||
|
CREATE TABLE IF NOT EXISTS import_jobs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
filename VARCHAR(255) NOT NULL,
|
||||||
|
file_key VARCHAR(255),
|
||||||
|
total_rows INTEGER NOT NULL,
|
||||||
|
processed_rows INTEGER DEFAULT 0,
|
||||||
|
successful_rows INTEGER DEFAULT 0,
|
||||||
|
failed_rows INTEGER DEFAULT 0,
|
||||||
|
status importjobstatus DEFAULT 'processing' NOT NULL,
|
||||||
|
errors JSONB DEFAULT '[]'::jsonb,
|
||||||
|
|
||||||
|
-- WordPress import enhancements
|
||||||
|
field_mapping JSONB DEFAULT '{}'::jsonb,
|
||||||
|
wordpress_metadata JSONB DEFAULT '{}'::jsonb,
|
||||||
|
imported_user_ids JSONB DEFAULT '[]'::jsonb,
|
||||||
|
rollback_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
rollback_by UUID REFERENCES users(id),
|
||||||
|
|
||||||
|
imported_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 2: Subscription & Payment Tables
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS subscription_plans (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
price_cents INTEGER NOT NULL,
|
||||||
|
billing_cycle VARCHAR(20) NOT NULL,
|
||||||
|
stripe_price_id VARCHAR(255),
|
||||||
|
custom_cycle_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
minimum_price_cents INTEGER DEFAULT 0,
|
||||||
|
allow_donation BOOLEAN DEFAULT FALSE,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS subscriptions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
plan_id UUID NOT NULL REFERENCES subscription_plans(id),
|
||||||
|
stripe_subscription_id VARCHAR(255),
|
||||||
|
stripe_customer_id VARCHAR(255),
|
||||||
|
base_subscription_cents INTEGER NOT NULL,
|
||||||
|
donation_cents INTEGER DEFAULT 0,
|
||||||
|
status subscriptionstatus DEFAULT 'active' NOT NULL,
|
||||||
|
current_period_start TIMESTAMP WITH TIME ZONE,
|
||||||
|
current_period_end TIMESTAMP WITH TIME ZONE,
|
||||||
|
cancel_at_period_end BOOLEAN DEFAULT FALSE,
|
||||||
|
canceled_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
manual_payment BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS donations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
amount_cents INTEGER NOT NULL,
|
||||||
|
donation_type donationtype NOT NULL,
|
||||||
|
status donationstatus DEFAULT 'pending' NOT NULL,
|
||||||
|
stripe_payment_intent_id VARCHAR(255),
|
||||||
|
donor_name VARCHAR(200),
|
||||||
|
donor_email VARCHAR(255),
|
||||||
|
message TEXT,
|
||||||
|
is_anonymous BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 3: RBAC Tables
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
code VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
name VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
module VARCHAR(50),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
is_system_role BOOLEAN DEFAULT FALSE,
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
role VARCHAR(50),
|
||||||
|
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_invitations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
role userrole NOT NULL,
|
||||||
|
token VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
invited_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
invited_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
accepted_by UUID REFERENCES users(id),
|
||||||
|
accepted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
status invitationstatus DEFAULT 'pending' NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 4: Document Management Tables
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS newsletter_archives (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
published_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
document_url TEXT NOT NULL,
|
||||||
|
document_type VARCHAR(50) NOT NULL,
|
||||||
|
created_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS financial_reports (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
year INTEGER NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
document_url TEXT NOT NULL,
|
||||||
|
document_type VARCHAR(50) NOT NULL,
|
||||||
|
created_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bylaws_documents (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
version VARCHAR(50) NOT NULL,
|
||||||
|
effective_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
document_url TEXT NOT NULL,
|
||||||
|
document_type VARCHAR(50) NOT NULL,
|
||||||
|
is_current BOOLEAN DEFAULT FALSE,
|
||||||
|
created_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 5: System Tables
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS storage_usage (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
total_bytes_used BIGINT DEFAULT 0,
|
||||||
|
max_bytes_allowed BIGINT,
|
||||||
|
last_calculated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS import_rollback_audit (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
import_job_id UUID NOT NULL REFERENCES import_jobs(id),
|
||||||
|
rolled_back_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
rolled_back_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
deleted_user_count INTEGER NOT NULL,
|
||||||
|
deleted_user_ids JSONB NOT NULL,
|
||||||
|
reason TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Initialize storage_usage with default row
|
||||||
|
INSERT INTO storage_usage (id, total_bytes_used, max_bytes_allowed)
|
||||||
|
VALUES (gen_random_uuid(), 0, 107374182400) -- 100GB limit
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 6: Create Indexes
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Users indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
|
||||||
|
|
||||||
|
-- Events indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_start_at ON events(start_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_published ON events(published);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_created_by ON events(created_by);
|
||||||
|
|
||||||
|
-- Event RSVPs indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_event_rsvps_event_id ON event_rsvps(event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_event_rsvps_user_id ON event_rsvps(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_event_rsvps_attended ON event_rsvps(attended);
|
||||||
|
|
||||||
|
-- Subscriptions indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_customer_id ON subscriptions(stripe_customer_id);
|
||||||
|
|
||||||
|
-- Permissions indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_permissions_code ON permissions(code);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_permissions_module ON permissions(module);
|
||||||
|
|
||||||
|
-- Roles indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_roles_code ON roles(code);
|
||||||
|
|
||||||
|
-- Role permissions indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_role_permissions_role ON role_permissions(role);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_role_permissions_role_id ON role_permissions(role_id);
|
||||||
|
|
||||||
|
-- User invitations indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_user_invitations_email ON user_invitations(email);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_user_invitations_token ON user_invitations(token);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
\echo '✅ All tables created successfully!'
|
||||||
|
\echo 'Run: psql ... -c "\dt" to verify'
|
||||||
80
migrations/diagnose_database.sql
Normal file
80
migrations/diagnose_database.sql
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Database Diagnostic Script
|
||||||
|
-- Run this to check what exists in your database
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
\echo '=== CHECKING ENUMS ==='
|
||||||
|
SELECT
|
||||||
|
t.typname as enum_name,
|
||||||
|
string_agg(e.enumlabel, ', ' ORDER BY e.enumsortorder) as values
|
||||||
|
FROM pg_type t
|
||||||
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
||||||
|
WHERE t.typname IN (
|
||||||
|
'userstatus', 'userrole', 'rsvpstatus', 'subscriptionstatus',
|
||||||
|
'donationtype', 'donationstatus', 'invitationstatus', 'importjobstatus'
|
||||||
|
)
|
||||||
|
GROUP BY t.typname
|
||||||
|
ORDER BY t.typname;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== CHECKING TABLES ==='
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
tablename
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
ORDER BY tablename;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== CHECKING USERS TABLE STRUCTURE ==='
|
||||||
|
SELECT
|
||||||
|
column_name,
|
||||||
|
data_type,
|
||||||
|
is_nullable,
|
||||||
|
column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== CHECKING FOR CRITICAL FIELDS ==='
|
||||||
|
\echo 'Checking if reminder tracking fields exist...'
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users'
|
||||||
|
AND column_name = 'email_verification_reminders_sent'
|
||||||
|
) as has_reminder_fields;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Checking if accepts_tos field exists (should be accepts_tos, not tos_accepted)...'
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users'
|
||||||
|
AND column_name IN ('accepts_tos', 'tos_accepted');
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Checking if WordPress import fields exist...'
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users'
|
||||||
|
AND column_name = 'import_source'
|
||||||
|
) as has_import_fields;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== CHECKING IMPORT_JOBS TABLE ==='
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'import_jobs'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== SUMMARY ==='
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM pg_type WHERE typname IN (
|
||||||
|
'userstatus', 'userrole', 'rsvpstatus', 'subscriptionstatus',
|
||||||
|
'donationtype', 'donationstatus', 'invitationstatus', 'importjobstatus'
|
||||||
|
)) as enum_count,
|
||||||
|
(SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'public') as table_count,
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'users') as users_column_count;
|
||||||
169
migrations/fix_missing_fields.sql
Normal file
169
migrations/fix_missing_fields.sql
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Fix Missing Fields Script
|
||||||
|
-- Safely adds missing fields without recreating existing structures
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
\echo '=== FIXING USERS TABLE ==='
|
||||||
|
|
||||||
|
-- Fix TOS field name if needed (tos_accepted -> accepts_tos)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'tos_accepted'
|
||||||
|
) AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'accepts_tos'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE users RENAME COLUMN tos_accepted TO accepts_tos;
|
||||||
|
RAISE NOTICE 'Renamed tos_accepted to accepts_tos';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add reminder tracking fields if missing
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'email_verification_reminders_sent'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE users ADD COLUMN email_verification_reminders_sent INTEGER DEFAULT 0 NOT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN last_email_verification_reminder_at TIMESTAMP WITH TIME ZONE;
|
||||||
|
ALTER TABLE users ADD COLUMN event_attendance_reminders_sent INTEGER DEFAULT 0 NOT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN last_event_attendance_reminder_at TIMESTAMP WITH TIME ZONE;
|
||||||
|
ALTER TABLE users ADD COLUMN payment_reminders_sent INTEGER DEFAULT 0 NOT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN last_payment_reminder_at TIMESTAMP WITH TIME ZONE;
|
||||||
|
ALTER TABLE users ADD COLUMN renewal_reminders_sent INTEGER DEFAULT 0 NOT NULL;
|
||||||
|
ALTER TABLE users ADD COLUMN last_renewal_reminder_at TIMESTAMP WITH TIME ZONE;
|
||||||
|
RAISE NOTICE 'Added reminder tracking fields';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add WordPress import fields if missing
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'import_source'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE users ADD COLUMN import_source VARCHAR(50);
|
||||||
|
ALTER TABLE users ADD COLUMN import_job_id UUID REFERENCES import_jobs(id);
|
||||||
|
ALTER TABLE users ADD COLUMN wordpress_user_id BIGINT;
|
||||||
|
ALTER TABLE users ADD COLUMN wordpress_registered_date TIMESTAMP WITH TIME ZONE;
|
||||||
|
RAISE NOTICE 'Added WordPress import tracking fields';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo '=== FIXING IMPORT_JOBS TABLE ==='
|
||||||
|
|
||||||
|
-- Add WordPress import enhancement fields if missing
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'import_jobs' AND column_name = 'field_mapping'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE import_jobs ADD COLUMN field_mapping JSONB DEFAULT '{}'::jsonb;
|
||||||
|
ALTER TABLE import_jobs ADD COLUMN wordpress_metadata JSONB DEFAULT '{}'::jsonb;
|
||||||
|
ALTER TABLE import_jobs ADD COLUMN imported_user_ids JSONB DEFAULT '[]'::jsonb;
|
||||||
|
ALTER TABLE import_jobs ADD COLUMN rollback_at TIMESTAMP WITH TIME ZONE;
|
||||||
|
ALTER TABLE import_jobs ADD COLUMN rollback_by UUID REFERENCES users(id);
|
||||||
|
RAISE NOTICE 'Added WordPress import enhancement fields to import_jobs';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add validating, preview_ready, rolled_back to ImportJobStatus enum if missing
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum e
|
||||||
|
JOIN pg_type t ON e.enumtypid = t.oid
|
||||||
|
WHERE t.typname = 'importjobstatus' AND e.enumlabel = 'validating'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE importjobstatus ADD VALUE IF NOT EXISTS 'validating';
|
||||||
|
RAISE NOTICE 'Added validating to importjobstatus enum';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum e
|
||||||
|
JOIN pg_type t ON e.enumtypid = t.oid
|
||||||
|
WHERE t.typname = 'importjobstatus' AND e.enumlabel = 'preview_ready'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE importjobstatus ADD VALUE IF NOT EXISTS 'preview_ready';
|
||||||
|
RAISE NOTICE 'Added preview_ready to importjobstatus enum';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum e
|
||||||
|
JOIN pg_type t ON e.enumtypid = t.oid
|
||||||
|
WHERE t.typname = 'importjobstatus' AND e.enumlabel = 'rolled_back'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE importjobstatus ADD VALUE IF NOT EXISTS 'rolled_back';
|
||||||
|
RAISE NOTICE 'Added rolled_back to importjobstatus enum';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add pending_validation, pre_validated, canceled, expired, abandoned to UserStatus enum if missing
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum e
|
||||||
|
JOIN pg_type t ON e.enumtypid = t.oid
|
||||||
|
WHERE t.typname = 'userstatus' AND e.enumlabel = 'pending_validation'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'pending_validation';
|
||||||
|
RAISE NOTICE 'Added pending_validation to userstatus enum';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum e
|
||||||
|
JOIN pg_type t ON e.enumtypid = t.oid
|
||||||
|
WHERE t.typname = 'userstatus' AND e.enumlabel = 'pre_validated'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'pre_validated';
|
||||||
|
RAISE NOTICE 'Added pre_validated to userstatus enum';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum e
|
||||||
|
JOIN pg_type t ON e.enumtypid = t.oid
|
||||||
|
WHERE t.typname = 'userstatus' AND e.enumlabel = 'canceled'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'canceled';
|
||||||
|
RAISE NOTICE 'Added canceled to userstatus enum';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum e
|
||||||
|
JOIN pg_type t ON e.enumtypid = t.oid
|
||||||
|
WHERE t.typname = 'userstatus' AND e.enumlabel = 'expired'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'expired';
|
||||||
|
RAISE NOTICE 'Added expired to userstatus enum';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum e
|
||||||
|
JOIN pg_type t ON e.enumtypid = t.oid
|
||||||
|
WHERE t.typname = 'userstatus' AND e.enumlabel = 'abandoned'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'abandoned';
|
||||||
|
RAISE NOTICE 'Added abandoned to userstatus enum';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== VERIFICATION ==='
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'users') as users_columns,
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'import_jobs') as import_jobs_columns,
|
||||||
|
(SELECT COUNT(*) FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'userstatus') as userstatus_values,
|
||||||
|
(SELECT COUNT(*) FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'importjobstatus') as importjobstatus_values;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '✅ Missing fields have been added!'
|
||||||
|
\echo 'You can now run: alembic stamp head'
|
||||||
238
migrations/seed_data.sql
Normal file
238
migrations/seed_data.sql
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Seed Data for LOAF Membership Platform
|
||||||
|
-- Run this after creating the database schema
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 1: Create Default Roles
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO roles (id, code, name, description, is_system_role, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(gen_random_uuid(), 'guest', 'Guest', 'Default role for new registrations', true, NOW(), NOW()),
|
||||||
|
(gen_random_uuid(), 'member', 'Member', 'Active paying members with full access', true, NOW(), NOW()),
|
||||||
|
(gen_random_uuid(), 'admin', 'Admin', 'Board members with management access', true, NOW(), NOW()),
|
||||||
|
(gen_random_uuid(), 'finance', 'Finance', 'Treasurer role with financial access', true, NOW(), NOW()),
|
||||||
|
(gen_random_uuid(), 'superadmin', 'Super Admin', 'Full system access', true, NOW(), NOW())
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 2: Create Permissions
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO permissions (id, code, name, description, module, created_at)
|
||||||
|
VALUES
|
||||||
|
-- User Management Permissions
|
||||||
|
(gen_random_uuid(), 'users.view', 'View Users', 'View user list and profiles', 'users', NOW()),
|
||||||
|
(gen_random_uuid(), 'users.create', 'Create Users', 'Create new users', 'users', NOW()),
|
||||||
|
(gen_random_uuid(), 'users.edit', 'Edit Users', 'Edit user information', 'users', NOW()),
|
||||||
|
(gen_random_uuid(), 'users.delete', 'Delete Users', 'Delete users', 'users', NOW()),
|
||||||
|
(gen_random_uuid(), 'users.approve', 'Approve Users', 'Approve pending memberships', 'users', NOW()),
|
||||||
|
(gen_random_uuid(), 'users.import', 'Import Users', 'Import users from CSV/external sources', 'users', NOW()),
|
||||||
|
|
||||||
|
-- Event Management Permissions
|
||||||
|
(gen_random_uuid(), 'events.view', 'View Events', 'View event list and details', 'events', NOW()),
|
||||||
|
(gen_random_uuid(), 'events.create', 'Create Events', 'Create new events', 'events', NOW()),
|
||||||
|
(gen_random_uuid(), 'events.edit', 'Edit Events', 'Edit event information', 'events', NOW()),
|
||||||
|
(gen_random_uuid(), 'events.delete', 'Delete Events', 'Delete events', 'events', NOW()),
|
||||||
|
(gen_random_uuid(), 'events.publish', 'Publish Events', 'Publish/unpublish events', 'events', NOW()),
|
||||||
|
(gen_random_uuid(), 'events.manage_attendance', 'Manage Attendance', 'Mark event attendance', 'events', NOW()),
|
||||||
|
|
||||||
|
-- Financial Permissions
|
||||||
|
(gen_random_uuid(), 'finance.view', 'View Financial Data', 'View subscriptions and payments', 'finance', NOW()),
|
||||||
|
(gen_random_uuid(), 'finance.manage_plans', 'Manage Subscription Plans', 'Create/edit subscription plans', 'finance', NOW()),
|
||||||
|
(gen_random_uuid(), 'finance.manage_subscriptions', 'Manage Subscriptions', 'Manage user subscriptions', 'finance', NOW()),
|
||||||
|
(gen_random_uuid(), 'finance.view_reports', 'View Financial Reports', 'Access financial reports', 'finance', NOW()),
|
||||||
|
(gen_random_uuid(), 'finance.export', 'Export Financial Data', 'Export financial data', 'finance', NOW()),
|
||||||
|
|
||||||
|
-- Content Management Permissions
|
||||||
|
(gen_random_uuid(), 'content.newsletters', 'Manage Newsletters', 'Manage newsletter archives', 'content', NOW()),
|
||||||
|
(gen_random_uuid(), 'content.documents', 'Manage Documents', 'Manage bylaws and documents', 'content', NOW()),
|
||||||
|
(gen_random_uuid(), 'content.gallery', 'Manage Gallery', 'Manage event galleries', 'content', NOW()),
|
||||||
|
|
||||||
|
-- System Permissions
|
||||||
|
(gen_random_uuid(), 'system.settings', 'System Settings', 'Manage system settings', 'system', NOW()),
|
||||||
|
(gen_random_uuid(), 'system.roles', 'Manage Roles', 'Create/edit roles and permissions', 'system', NOW()),
|
||||||
|
(gen_random_uuid(), 'system.invitations', 'Manage Invitations', 'Send admin invitations', 'system', NOW()),
|
||||||
|
(gen_random_uuid(), 'system.storage', 'Manage Storage', 'View storage usage', 'system', NOW()),
|
||||||
|
(gen_random_uuid(), 'system.audit', 'View Audit Logs', 'View system audit logs', 'system', NOW())
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 3: Assign Permissions to Roles
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Guest Role: No permissions (view-only through public pages)
|
||||||
|
-- No entries needed
|
||||||
|
|
||||||
|
-- Member Role: Limited permissions
|
||||||
|
INSERT INTO role_permissions (id, role, role_id, permission_id, created_at)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
'member',
|
||||||
|
(SELECT id FROM roles WHERE code = 'member'),
|
||||||
|
p.id,
|
||||||
|
NOW()
|
||||||
|
FROM permissions p
|
||||||
|
WHERE p.code IN (
|
||||||
|
'events.view'
|
||||||
|
)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Admin Role: Most permissions except financial
|
||||||
|
INSERT INTO role_permissions (id, role, role_id, permission_id, created_at)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
'admin',
|
||||||
|
(SELECT id FROM roles WHERE code = 'admin'),
|
||||||
|
p.id,
|
||||||
|
NOW()
|
||||||
|
FROM permissions p
|
||||||
|
WHERE p.code IN (
|
||||||
|
-- User Management
|
||||||
|
'users.view', 'users.create', 'users.edit', 'users.approve', 'users.import',
|
||||||
|
-- Event Management
|
||||||
|
'events.view', 'events.create', 'events.edit', 'events.delete', 'events.publish', 'events.manage_attendance',
|
||||||
|
-- Content Management
|
||||||
|
'content.newsletters', 'content.documents', 'content.gallery',
|
||||||
|
-- System (limited)
|
||||||
|
'system.invitations', 'system.storage'
|
||||||
|
)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Finance Role: Financial permissions + basic access
|
||||||
|
INSERT INTO role_permissions (id, role, role_id, permission_id, created_at)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
'finance',
|
||||||
|
(SELECT id FROM roles WHERE code = 'finance'),
|
||||||
|
p.id,
|
||||||
|
NOW()
|
||||||
|
FROM permissions p
|
||||||
|
WHERE p.code IN (
|
||||||
|
-- Financial
|
||||||
|
'finance.view', 'finance.manage_plans', 'finance.manage_subscriptions', 'finance.view_reports', 'finance.export',
|
||||||
|
-- Basic Access
|
||||||
|
'users.view', 'events.view'
|
||||||
|
)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Superadmin Role: All permissions
|
||||||
|
INSERT INTO role_permissions (id, role, role_id, permission_id, created_at)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
'superadmin',
|
||||||
|
(SELECT id FROM roles WHERE code = 'superadmin'),
|
||||||
|
p.id,
|
||||||
|
NOW()
|
||||||
|
FROM permissions p
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 4: Create Subscription Plans
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO subscription_plans (id, name, description, price_cents, billing_cycle, custom_cycle_enabled, minimum_price_cents, allow_donation, is_active, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
-- Annual Individual Membership
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'Annual Individual Membership',
|
||||||
|
'Standard annual membership for one person. Includes access to all LOAF events, member directory, and exclusive content.',
|
||||||
|
6000, -- $60.00
|
||||||
|
'annual',
|
||||||
|
false,
|
||||||
|
6000,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Annual Group Membership
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'Annual Group Membership',
|
||||||
|
'Annual membership for two people living at the same address. Both members receive full access to all LOAF benefits.',
|
||||||
|
10000, -- $100.00
|
||||||
|
'annual',
|
||||||
|
false,
|
||||||
|
10000,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Pay What You Want (with minimum)
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'Pay What You Want Membership',
|
||||||
|
'Choose your own annual membership amount. Minimum $30. Additional contributions help support our scholarship fund.',
|
||||||
|
3000, -- $30.00 minimum
|
||||||
|
'annual',
|
||||||
|
true, -- Allow custom amount
|
||||||
|
3000, -- Minimum $30
|
||||||
|
true, -- Additional amount is treated as donation
|
||||||
|
true,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STEP 5: Initialize Storage Usage (if not already done)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO storage_usage (id, total_bytes_used, max_bytes_allowed, last_calculated_at, created_at, updated_at)
|
||||||
|
VALUES (gen_random_uuid(), 0, 107374182400, NOW(), NOW(), NOW()) -- 100GB limit
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Success Message
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
\echo '✅ Seed data created successfully!'
|
||||||
|
\echo ''
|
||||||
|
\echo 'Created:'
|
||||||
|
\echo ' - 5 default roles (guest, member, admin, finance, superadmin)'
|
||||||
|
\echo ' - 25 permissions across 5 modules'
|
||||||
|
\echo ' - Role-permission mappings'
|
||||||
|
\echo ' - 3 subscription plans'
|
||||||
|
\echo ' - Storage usage initialization'
|
||||||
|
\echo ''
|
||||||
|
\echo 'Next steps:'
|
||||||
|
\echo ' 1. Create superadmin user (see instructions below)'
|
||||||
|
\echo ' 2. Configure Stripe price IDs in subscription_plans'
|
||||||
|
\echo ' 3. Start the application'
|
||||||
|
\echo ''
|
||||||
|
\echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
||||||
|
\echo 'CREATE SUPERADMIN USER:'
|
||||||
|
\echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
||||||
|
\echo ''
|
||||||
|
\echo 'Generate password hash in Python:'
|
||||||
|
\echo ' python3 -c "import bcrypt; print(bcrypt.hashpw(b\"your-password\", bcrypt.gensalt()).decode())"'
|
||||||
|
\echo ''
|
||||||
|
\echo 'Then run:'
|
||||||
|
\echo ' psql -U postgres -d loaf_new'
|
||||||
|
\echo ''
|
||||||
|
\echo 'INSERT INTO users ('
|
||||||
|
\echo ' id, email, password_hash, first_name, last_name,'
|
||||||
|
\echo ' status, role, email_verified, created_at, updated_at'
|
||||||
|
\echo ') VALUES ('
|
||||||
|
\echo ' gen_random_uuid(),'
|
||||||
|
\echo ' '\''admin@loafmembers.org'\'','
|
||||||
|
\echo ' '\''$2b$12$YOUR_BCRYPT_HASH_HERE'\'','
|
||||||
|
\echo ' '\''Admin'\'','
|
||||||
|
\echo ' '\''User'\'','
|
||||||
|
\echo ' '\''active'\'','
|
||||||
|
\echo ' '\''superadmin'\'','
|
||||||
|
\echo ' true,'
|
||||||
|
\echo ' NOW(),'
|
||||||
|
\echo ' NOW()'
|
||||||
|
\echo ');'
|
||||||
|
\echo ''
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
aiosmtplib==5.0.0
|
aiosmtplib==5.0.0
|
||||||
|
alembic==1.14.0
|
||||||
annotated-types==0.7.0
|
annotated-types==0.7.0
|
||||||
anyio==4.11.0
|
anyio==4.11.0
|
||||||
bcrypt==4.1.3
|
bcrypt==4.1.3
|
||||||
|
|||||||
Reference in New Issue
Block a user