forked from andika/membership-be
Merge pull request 'Merge to LOAF-PROD for Demo' (#27) from dev into loaf-prod
Reviewed-on: andika/membership-be#27
This commit is contained in:
83
.dockerignore
Normal file
83
.dockerignore
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
|
||||||
|
# Environment files (will be mounted or passed via env vars)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Alembic
|
||||||
|
alembic/versions/__pycache__/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose*.yml
|
||||||
|
.docker/
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Uploads (will be mounted as volume)
|
||||||
|
uploads/
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -245,6 +245,9 @@ temp_uploads/
|
|||||||
tmp/
|
tmp/
|
||||||
temporary/
|
temporary/
|
||||||
|
|
||||||
|
# Generated SQL files (from scripts)
|
||||||
|
create_superadmin.sql
|
||||||
|
|
||||||
# CSV imports
|
# CSV imports
|
||||||
imports/*.csv
|
imports/*.csv
|
||||||
!imports/.gitkeep
|
!imports/.gitkeep
|
||||||
|
|||||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Backend Dockerfile - FastAPI with Python
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
|
||||||
|
# Set work directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
libpq-dev \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN adduser --disabled-password --gecos '' appuser && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8000/health || exit 1
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
141
add_directory_permissions.py
Normal file
141
add_directory_permissions.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Add Directory Permissions Script
|
||||||
|
|
||||||
|
This script adds the new directory.view and directory.manage permissions
|
||||||
|
without clearing existing permissions.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python add_directory_permissions.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from database import Base
|
||||||
|
from models import Permission, RolePermission, Role, UserRole
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL")
|
||||||
|
if not DATABASE_URL:
|
||||||
|
print("Error: DATABASE_URL environment variable not set")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# New directory permissions
|
||||||
|
NEW_PERMISSIONS = [
|
||||||
|
{"code": "directory.view", "name": "View Directory Settings", "description": "View member directory field configuration", "module": "directory"},
|
||||||
|
{"code": "directory.manage", "name": "Manage Directory Fields", "description": "Enable/disable directory fields shown in Profile and Directory pages", "module": "directory"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Roles that should have these permissions
|
||||||
|
ROLE_PERMISSION_MAP = {
|
||||||
|
"directory.view": ["admin", "superadmin"],
|
||||||
|
"directory.manage": ["admin", "superadmin"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_directory_permissions():
|
||||||
|
"""Add directory permissions and assign to appropriate roles"""
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("=" * 60)
|
||||||
|
print("Adding Directory Permissions")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Step 1: Add permissions if they don't exist
|
||||||
|
print("\n1. Adding permissions...")
|
||||||
|
permission_map = {}
|
||||||
|
|
||||||
|
for perm_data in NEW_PERMISSIONS:
|
||||||
|
existing = db.query(Permission).filter(Permission.code == perm_data["code"]).first()
|
||||||
|
if existing:
|
||||||
|
print(f" - {perm_data['code']}: Already exists")
|
||||||
|
permission_map[perm_data["code"]] = existing
|
||||||
|
else:
|
||||||
|
permission = Permission(
|
||||||
|
code=perm_data["code"],
|
||||||
|
name=perm_data["name"],
|
||||||
|
description=perm_data["description"],
|
||||||
|
module=perm_data["module"]
|
||||||
|
)
|
||||||
|
db.add(permission)
|
||||||
|
db.flush() # Get the ID
|
||||||
|
permission_map[perm_data["code"]] = permission
|
||||||
|
print(f" - {perm_data['code']}: Created")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Step 2: Get roles
|
||||||
|
print("\n2. Fetching roles...")
|
||||||
|
roles = db.query(Role).all()
|
||||||
|
role_map = {role.code: role for role in roles}
|
||||||
|
print(f" Found {len(roles)} roles: {', '.join(role_map.keys())}")
|
||||||
|
|
||||||
|
# Enum mapping for backward compatibility
|
||||||
|
role_enum_map = {
|
||||||
|
'guest': UserRole.guest,
|
||||||
|
'member': UserRole.member,
|
||||||
|
'admin': UserRole.admin,
|
||||||
|
'superadmin': UserRole.superadmin,
|
||||||
|
'finance': UserRole.finance
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 3: Assign permissions to roles
|
||||||
|
print("\n3. Assigning permissions to roles...")
|
||||||
|
for perm_code, role_codes in ROLE_PERMISSION_MAP.items():
|
||||||
|
permission = permission_map.get(perm_code)
|
||||||
|
if not permission:
|
||||||
|
print(f" Warning: Permission {perm_code} not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for role_code in role_codes:
|
||||||
|
role = role_map.get(role_code)
|
||||||
|
if not role:
|
||||||
|
print(f" Warning: Role {role_code} not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if mapping already exists
|
||||||
|
existing_mapping = db.query(RolePermission).filter(
|
||||||
|
RolePermission.role_id == role.id,
|
||||||
|
RolePermission.permission_id == permission.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_mapping:
|
||||||
|
print(f" - {role_code} -> {perm_code}: Already assigned")
|
||||||
|
else:
|
||||||
|
role_enum = role_enum_map.get(role_code, UserRole.guest)
|
||||||
|
mapping = RolePermission(
|
||||||
|
role=role_enum,
|
||||||
|
role_id=role.id,
|
||||||
|
permission_id=permission.id
|
||||||
|
)
|
||||||
|
db.add(mapping)
|
||||||
|
print(f" - {role_code} -> {perm_code}: Assigned")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Directory permissions added successfully!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print(f"\nError: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
add_directory_permissions()
|
||||||
141
add_registration_permissions.py
Normal file
141
add_registration_permissions.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Add Registration Permissions Script
|
||||||
|
|
||||||
|
This script adds the new registration.view and registration.manage permissions
|
||||||
|
without clearing existing permissions.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python add_registration_permissions.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from database import Base
|
||||||
|
from models import Permission, RolePermission, Role, UserRole
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL")
|
||||||
|
if not DATABASE_URL:
|
||||||
|
print("Error: DATABASE_URL environment variable not set")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# New registration permissions
|
||||||
|
NEW_PERMISSIONS = [
|
||||||
|
{"code": "registration.view", "name": "View Registration Settings", "description": "View registration form schema and settings", "module": "registration"},
|
||||||
|
{"code": "registration.manage", "name": "Manage Registration Form", "description": "Edit registration form schema, steps, and fields", "module": "registration"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Roles that should have these permissions
|
||||||
|
ROLE_PERMISSION_MAP = {
|
||||||
|
"registration.view": ["admin", "superadmin"],
|
||||||
|
"registration.manage": ["admin", "superadmin"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_registration_permissions():
|
||||||
|
"""Add registration permissions and assign to appropriate roles"""
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("=" * 60)
|
||||||
|
print("Adding Registration Permissions")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Step 1: Add permissions if they don't exist
|
||||||
|
print("\n1. Adding permissions...")
|
||||||
|
permission_map = {}
|
||||||
|
|
||||||
|
for perm_data in NEW_PERMISSIONS:
|
||||||
|
existing = db.query(Permission).filter(Permission.code == perm_data["code"]).first()
|
||||||
|
if existing:
|
||||||
|
print(f" - {perm_data['code']}: Already exists")
|
||||||
|
permission_map[perm_data["code"]] = existing
|
||||||
|
else:
|
||||||
|
permission = Permission(
|
||||||
|
code=perm_data["code"],
|
||||||
|
name=perm_data["name"],
|
||||||
|
description=perm_data["description"],
|
||||||
|
module=perm_data["module"]
|
||||||
|
)
|
||||||
|
db.add(permission)
|
||||||
|
db.flush() # Get the ID
|
||||||
|
permission_map[perm_data["code"]] = permission
|
||||||
|
print(f" - {perm_data['code']}: Created")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Step 2: Get roles
|
||||||
|
print("\n2. Fetching roles...")
|
||||||
|
roles = db.query(Role).all()
|
||||||
|
role_map = {role.code: role for role in roles}
|
||||||
|
print(f" Found {len(roles)} roles: {', '.join(role_map.keys())}")
|
||||||
|
|
||||||
|
# Enum mapping for backward compatibility
|
||||||
|
role_enum_map = {
|
||||||
|
'guest': UserRole.guest,
|
||||||
|
'member': UserRole.member,
|
||||||
|
'admin': UserRole.admin,
|
||||||
|
'superadmin': UserRole.superadmin,
|
||||||
|
'finance': UserRole.finance
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 3: Assign permissions to roles
|
||||||
|
print("\n3. Assigning permissions to roles...")
|
||||||
|
for perm_code, role_codes in ROLE_PERMISSION_MAP.items():
|
||||||
|
permission = permission_map.get(perm_code)
|
||||||
|
if not permission:
|
||||||
|
print(f" Warning: Permission {perm_code} not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for role_code in role_codes:
|
||||||
|
role = role_map.get(role_code)
|
||||||
|
if not role:
|
||||||
|
print(f" Warning: Role {role_code} not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if mapping already exists
|
||||||
|
existing_mapping = db.query(RolePermission).filter(
|
||||||
|
RolePermission.role_id == role.id,
|
||||||
|
RolePermission.permission_id == permission.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_mapping:
|
||||||
|
print(f" - {role_code} -> {perm_code}: Already assigned")
|
||||||
|
else:
|
||||||
|
role_enum = role_enum_map.get(role_code, UserRole.guest)
|
||||||
|
mapping = RolePermission(
|
||||||
|
role=role_enum,
|
||||||
|
role_id=role.id,
|
||||||
|
permission_id=permission.id
|
||||||
|
)
|
||||||
|
db.add(mapping)
|
||||||
|
print(f" - {role_code} -> {perm_code}: Assigned")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Registration permissions added successfully!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print(f"\nError: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
add_registration_permissions()
|
||||||
39
alembic/versions/014_add_custom_registration_data.py
Normal file
39
alembic/versions/014_add_custom_registration_data.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""add_custom_registration_data
|
||||||
|
|
||||||
|
Revision ID: 014_custom_registration
|
||||||
|
Revises: a1b2c3d4e5f6
|
||||||
|
Create Date: 2026-02-01 10:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '014_custom_registration'
|
||||||
|
down_revision: Union[str, None] = 'a1b2c3d4e5f6'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add custom_registration_data column to users table
|
||||||
|
# This stores dynamic registration field responses as JSON
|
||||||
|
op.add_column('users', sa.Column(
|
||||||
|
'custom_registration_data',
|
||||||
|
sa.JSON,
|
||||||
|
nullable=False,
|
||||||
|
server_default='{}'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Add comment for documentation
|
||||||
|
op.execute("""
|
||||||
|
COMMENT ON COLUMN users.custom_registration_data IS
|
||||||
|
'Dynamic registration field responses stored as JSON for custom form fields';
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('users', 'custom_registration_data')
|
||||||
100
alembic/versions/add_payment_methods.py
Normal file
100
alembic/versions/add_payment_methods.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
"""add_payment_methods
|
||||||
|
|
||||||
|
Revision ID: a1b2c3d4e5f6
|
||||||
|
Revises: 956ea1628264
|
||||||
|
Create Date: 2026-01-30 10:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'a1b2c3d4e5f6'
|
||||||
|
down_revision: Union[str, None] = '956ea1628264'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
# Create PaymentMethodType enum
|
||||||
|
paymentmethodtype = postgresql.ENUM(
|
||||||
|
'card', 'cash', 'bank_transfer', 'check',
|
||||||
|
name='paymentmethodtype',
|
||||||
|
create_type=False
|
||||||
|
)
|
||||||
|
paymentmethodtype.create(conn, checkfirst=True)
|
||||||
|
|
||||||
|
# Check if stripe_customer_id column exists on users table
|
||||||
|
result = conn.execute(sa.text("""
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'stripe_customer_id'
|
||||||
|
"""))
|
||||||
|
if result.fetchone() is None:
|
||||||
|
# Add stripe_customer_id to users table
|
||||||
|
op.add_column('users', sa.Column(
|
||||||
|
'stripe_customer_id',
|
||||||
|
sa.String(),
|
||||||
|
nullable=True,
|
||||||
|
comment='Stripe Customer ID for payment method management'
|
||||||
|
))
|
||||||
|
op.create_index('ix_users_stripe_customer_id', 'users', ['stripe_customer_id'])
|
||||||
|
|
||||||
|
# Check if payment_methods table exists
|
||||||
|
result = conn.execute(sa.text("""
|
||||||
|
SELECT table_name FROM information_schema.tables
|
||||||
|
WHERE table_name = 'payment_methods'
|
||||||
|
"""))
|
||||||
|
if result.fetchone() is None:
|
||||||
|
# Create payment_methods table
|
||||||
|
op.create_table(
|
||||||
|
'payment_methods',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
|
||||||
|
sa.Column('stripe_payment_method_id', sa.String(), nullable=True, unique=True, comment='Stripe pm_xxx reference'),
|
||||||
|
sa.Column('card_brand', sa.String(20), nullable=True, comment='Card brand: visa, mastercard, amex, etc.'),
|
||||||
|
sa.Column('card_last4', sa.String(4), nullable=True, comment='Last 4 digits of card'),
|
||||||
|
sa.Column('card_exp_month', sa.Integer(), nullable=True, comment='Card expiration month'),
|
||||||
|
sa.Column('card_exp_year', sa.Integer(), nullable=True, comment='Card expiration year'),
|
||||||
|
sa.Column('card_funding', sa.String(20), nullable=True, comment='Card funding type: credit, debit, prepaid'),
|
||||||
|
sa.Column('payment_type', paymentmethodtype, nullable=False, server_default='card'),
|
||||||
|
sa.Column('is_default', sa.Boolean(), nullable=False, server_default='false', comment='Whether this is the default payment method for auto-renewals'),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true', comment='Soft delete flag - False means removed'),
|
||||||
|
sa.Column('is_manual', sa.Boolean(), nullable=False, server_default='false', comment='True for manually recorded methods (cash/check)'),
|
||||||
|
sa.Column('manual_notes', sa.Text(), nullable=True, comment='Admin notes for manual payment methods'),
|
||||||
|
sa.Column('created_by', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True, comment='Admin who added this on behalf of user'),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create indexes
|
||||||
|
op.create_index('ix_payment_methods_user_id', 'payment_methods', ['user_id'])
|
||||||
|
op.create_index('ix_payment_methods_stripe_pm_id', 'payment_methods', ['stripe_payment_method_id'])
|
||||||
|
op.create_index('idx_payment_method_user_default', 'payment_methods', ['user_id', 'is_default'])
|
||||||
|
op.create_index('idx_payment_method_active', 'payment_methods', ['user_id', 'is_active'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop indexes
|
||||||
|
op.drop_index('idx_payment_method_active', table_name='payment_methods')
|
||||||
|
op.drop_index('idx_payment_method_user_default', table_name='payment_methods')
|
||||||
|
op.drop_index('ix_payment_methods_stripe_pm_id', table_name='payment_methods')
|
||||||
|
op.drop_index('ix_payment_methods_user_id', table_name='payment_methods')
|
||||||
|
|
||||||
|
# Drop payment_methods table
|
||||||
|
op.drop_table('payment_methods')
|
||||||
|
|
||||||
|
# Drop stripe_customer_id from users
|
||||||
|
op.drop_index('ix_users_stripe_customer_id', table_name='users')
|
||||||
|
op.drop_column('users', 'stripe_customer_id')
|
||||||
|
|
||||||
|
# Drop PaymentMethodType enum
|
||||||
|
paymentmethodtype = postgresql.ENUM(
|
||||||
|
'card', 'cash', 'bank_transfer', 'check',
|
||||||
|
name='paymentmethodtype'
|
||||||
|
)
|
||||||
|
paymentmethodtype.drop(op.get_bind(), checkfirst=True)
|
||||||
4
auth.py
4
auth.py
@@ -128,7 +128,7 @@ async def get_current_admin_user(current_user: User = Depends(get_current_user))
|
|||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
async def get_active_member(current_user: User = Depends(get_current_user)) -> User:
|
async def get_active_member(current_user: User = Depends(get_current_user)) -> User:
|
||||||
"""Require user to be active member with valid payment"""
|
"""Require user to be active member or staff with valid status"""
|
||||||
from models import UserStatus
|
from models import UserStatus
|
||||||
|
|
||||||
if current_user.status != UserStatus.active:
|
if current_user.status != UserStatus.active:
|
||||||
@@ -138,7 +138,7 @@ async def get_active_member(current_user: User = Depends(get_current_user)) -> U
|
|||||||
)
|
)
|
||||||
|
|
||||||
role_code = get_user_role_code(current_user)
|
role_code = get_user_role_code(current_user)
|
||||||
if role_code not in ["member", "admin", "superadmin"]:
|
if role_code not in ["member", "admin", "superadmin", "finance"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Member access only"
|
detail="Member access only"
|
||||||
|
|||||||
@@ -1,38 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Create Superadmin User Script
|
Create Superadmin User Script
|
||||||
Generates a superadmin user with hashed password for LOAF membership platform
|
Directly creates a superadmin user in the database for LOAF membership platform
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import bcrypt
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
|
|
||||||
def generate_password_hash(password: str) -> str:
|
# Add the backend directory to path for imports
|
||||||
"""Generate bcrypt hash for password"""
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
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():
|
def main():
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
@@ -40,6 +17,15 @@ def main():
|
|||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
# Check for DATABASE_URL
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
database_url = os.getenv("DATABASE_URL")
|
||||||
|
if not database_url:
|
||||||
|
print("❌ DATABASE_URL not found in environment or .env file")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Get user input
|
# Get user input
|
||||||
email = input("Email address: ").strip()
|
email = input("Email address: ").strip()
|
||||||
if not email or '@' not in email:
|
if not email or '@' not in email:
|
||||||
@@ -68,31 +54,89 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("Generating password hash...")
|
print("Creating superadmin user...")
|
||||||
password_hash = generate_password_hash(password)
|
|
||||||
|
|
||||||
print("✅ Password hash generated")
|
try:
|
||||||
print()
|
# Import database dependencies
|
||||||
print("=" * 70)
|
from sqlalchemy import create_engine, text
|
||||||
print("SQL STATEMENT")
|
from passlib.context import CryptContext
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
sql = generate_sql(email, password_hash, first_name, last_name)
|
# Create password hash
|
||||||
print(sql)
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
password_hash = pwd_context.hash(password)
|
||||||
|
|
||||||
# Save to file
|
# Connect to database
|
||||||
output_file = "create_superadmin.sql"
|
engine = create_engine(database_url)
|
||||||
with open(output_file, 'w') as f:
|
|
||||||
f.write(sql)
|
|
||||||
|
|
||||||
print("=" * 70)
|
with engine.connect() as conn:
|
||||||
print(f"✅ SQL saved to: {output_file}")
|
# Check if user already exists
|
||||||
print()
|
result = conn.execute(
|
||||||
print("Run this command to create the user:")
|
text("SELECT id FROM users WHERE email = :email"),
|
||||||
print(f" psql -U postgres -d loaf_new -f {output_file}")
|
{"email": email}
|
||||||
print()
|
)
|
||||||
print("Or copy the SQL above and run it directly in psql")
|
if result.fetchone():
|
||||||
print("=" * 70)
|
print(f"❌ User with email '{email}' already exists")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Insert superadmin user
|
||||||
|
conn.execute(
|
||||||
|
text("""
|
||||||
|
INSERT INTO users (
|
||||||
|
id, email, password_hash, first_name, last_name,
|
||||||
|
phone, address, city, state, zipcode, date_of_birth,
|
||||||
|
status, role, email_verified,
|
||||||
|
newsletter_subscribed, accepts_tos,
|
||||||
|
created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
gen_random_uuid(),
|
||||||
|
:email,
|
||||||
|
:password_hash,
|
||||||
|
:first_name,
|
||||||
|
:last_name,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'1990-01-01',
|
||||||
|
'active',
|
||||||
|
'superadmin',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"email": email,
|
||||||
|
"password_hash": password_hash,
|
||||||
|
"first_name": first_name,
|
||||||
|
"last_name": last_name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print("✅ Superadmin user created successfully!")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
print(f" Email: {email}")
|
||||||
|
print(f" Name: {first_name} {last_name}")
|
||||||
|
print(f" Role: superadmin")
|
||||||
|
print(f" Status: active")
|
||||||
|
print()
|
||||||
|
print("You can now log in with these credentials.")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"❌ Missing dependency: {e}")
|
||||||
|
print(" Run: pip install sqlalchemy psycopg2-binary passlib python-dotenv")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Database error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
@@ -100,6 +144,3 @@ if __name__ == "__main__":
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\n❌ Cancelled by user")
|
print("\n\n❌ Cancelled by user")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as e:
|
|
||||||
print(f"\n❌ Error: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|||||||
@@ -94,6 +94,30 @@ BEGIN;
|
|||||||
-- SECTION 2: Create Core Tables
|
-- SECTION 2: Create Core Tables
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Import Jobs table (must be created before users due to FK reference)
|
||||||
|
CREATE TABLE IF NOT EXISTS import_jobs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
filename VARCHAR NOT NULL,
|
||||||
|
status importjobstatus NOT NULL DEFAULT 'processing',
|
||||||
|
total_rows INTEGER DEFAULT 0,
|
||||||
|
processed_rows INTEGER DEFAULT 0,
|
||||||
|
success_count INTEGER DEFAULT 0,
|
||||||
|
error_count INTEGER DEFAULT 0,
|
||||||
|
error_log 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, -- Will be updated with FK after users table exists
|
||||||
|
|
||||||
|
started_by UUID, -- Will be updated with FK after users table exists
|
||||||
|
started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
-- Users table
|
-- Users table
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
@@ -103,6 +127,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
password_hash VARCHAR NOT NULL,
|
password_hash VARCHAR NOT NULL,
|
||||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
email_verification_token VARCHAR UNIQUE,
|
email_verification_token VARCHAR UNIQUE,
|
||||||
|
email_verification_expires TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
-- Personal Information
|
-- Personal Information
|
||||||
first_name VARCHAR NOT NULL,
|
first_name VARCHAR NOT NULL,
|
||||||
@@ -113,7 +138,6 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
state VARCHAR(2),
|
state VARCHAR(2),
|
||||||
zipcode VARCHAR(10),
|
zipcode VARCHAR(10),
|
||||||
date_of_birth DATE,
|
date_of_birth DATE,
|
||||||
bio TEXT,
|
|
||||||
|
|
||||||
-- Profile
|
-- Profile
|
||||||
profile_photo_url VARCHAR,
|
profile_photo_url VARCHAR,
|
||||||
@@ -137,20 +161,44 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
-- Status & Role
|
-- Status & Role
|
||||||
status userstatus NOT NULL DEFAULT 'pending_email',
|
status userstatus NOT NULL DEFAULT 'pending_email',
|
||||||
role userrole NOT NULL DEFAULT 'guest',
|
role userrole NOT NULL DEFAULT 'guest',
|
||||||
role_id UUID, -- For dynamic RBAC (added in later migration)
|
role_id UUID, -- For dynamic RBAC
|
||||||
|
|
||||||
-- Rejection Tracking
|
-- Newsletter Preferences
|
||||||
rejection_reason TEXT,
|
newsletter_subscribed BOOLEAN DEFAULT TRUE,
|
||||||
rejected_at TIMESTAMP WITH TIME ZONE,
|
newsletter_publish_name BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
rejected_by UUID REFERENCES users(id),
|
newsletter_publish_photo BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
newsletter_publish_birthday BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
newsletter_publish_none BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
|
||||||
|
-- Volunteer Interests
|
||||||
|
volunteer_interests JSONB DEFAULT '[]'::jsonb,
|
||||||
|
|
||||||
|
-- Scholarship Request
|
||||||
|
scholarship_requested BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
scholarship_reason TEXT,
|
||||||
|
|
||||||
|
-- Directory Settings
|
||||||
|
show_in_directory BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
directory_email VARCHAR,
|
||||||
|
directory_bio TEXT,
|
||||||
|
directory_address VARCHAR,
|
||||||
|
directory_phone VARCHAR,
|
||||||
|
directory_dob DATE,
|
||||||
|
directory_partner_name VARCHAR,
|
||||||
|
|
||||||
|
-- Password Reset
|
||||||
|
password_reset_token VARCHAR,
|
||||||
|
password_reset_expires TIMESTAMP WITH TIME ZONE,
|
||||||
|
force_password_change BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
|
||||||
|
-- Terms of Service
|
||||||
|
accepts_tos BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
tos_accepted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
-- Membership
|
-- Membership
|
||||||
member_since DATE,
|
member_since DATE,
|
||||||
accepts_tos BOOLEAN DEFAULT FALSE,
|
|
||||||
tos_accepted_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
newsletter_subscribed BOOLEAN DEFAULT TRUE,
|
|
||||||
|
|
||||||
-- Reminder Tracking (from migration 004)
|
-- Reminder Tracking
|
||||||
email_verification_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
email_verification_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
last_email_verification_reminder_at TIMESTAMP WITH TIME ZONE,
|
last_email_verification_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
event_attendance_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
event_attendance_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
@@ -160,12 +208,21 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
renewal_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
renewal_reminders_sent INTEGER DEFAULT 0 NOT NULL,
|
||||||
last_renewal_reminder_at TIMESTAMP WITH TIME ZONE,
|
last_renewal_reminder_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- Rejection Tracking
|
||||||
|
rejection_reason TEXT,
|
||||||
|
rejected_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
rejected_by UUID REFERENCES users(id),
|
||||||
|
|
||||||
-- WordPress Import Tracking
|
-- WordPress Import Tracking
|
||||||
import_source VARCHAR(50),
|
import_source VARCHAR(50),
|
||||||
import_job_id UUID REFERENCES import_jobs(id),
|
import_job_id UUID REFERENCES import_jobs(id),
|
||||||
wordpress_user_id BIGINT,
|
wordpress_user_id BIGINT,
|
||||||
wordpress_registered_date TIMESTAMP WITH TIME ZONE,
|
wordpress_registered_date TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- Role Change Audit Trail
|
||||||
|
role_changed_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
role_changed_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
-- Timestamps
|
-- Timestamps
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -255,11 +312,23 @@ CREATE TABLE IF NOT EXISTS subscription_plans (
|
|||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
price_cents INTEGER NOT NULL,
|
price_cents INTEGER NOT NULL,
|
||||||
billing_cycle VARCHAR NOT NULL DEFAULT 'annual',
|
billing_cycle VARCHAR NOT NULL DEFAULT 'yearly',
|
||||||
|
stripe_price_id VARCHAR, -- Legacy, deprecated
|
||||||
|
|
||||||
-- Configuration
|
-- Configuration
|
||||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
features JSONB DEFAULT '[]'::jsonb,
|
|
||||||
|
-- Custom billing cycle fields (for recurring date ranges like Jan 1 - Dec 31)
|
||||||
|
custom_cycle_enabled BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
custom_cycle_start_month INTEGER,
|
||||||
|
custom_cycle_start_day INTEGER,
|
||||||
|
custom_cycle_end_month INTEGER,
|
||||||
|
custom_cycle_end_day INTEGER,
|
||||||
|
|
||||||
|
-- Dynamic pricing fields
|
||||||
|
minimum_price_cents INTEGER DEFAULT 3000 NOT NULL,
|
||||||
|
suggested_price_cents INTEGER,
|
||||||
|
allow_donation BOOLEAN DEFAULT TRUE NOT NULL,
|
||||||
|
|
||||||
-- Timestamps
|
-- Timestamps
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -281,13 +350,21 @@ CREATE TABLE IF NOT EXISTS subscriptions (
|
|||||||
status subscriptionstatus DEFAULT 'active',
|
status subscriptionstatus DEFAULT 'active',
|
||||||
start_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
start_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
end_date TIMESTAMP WITH TIME ZONE,
|
end_date TIMESTAMP WITH TIME ZONE,
|
||||||
next_billing_date TIMESTAMP WITH TIME ZONE,
|
|
||||||
|
|
||||||
-- Payment Details
|
-- Payment Details
|
||||||
amount_paid_cents INTEGER,
|
amount_paid_cents INTEGER,
|
||||||
base_subscription_cents INTEGER NOT NULL,
|
base_subscription_cents INTEGER NOT NULL,
|
||||||
donation_cents INTEGER DEFAULT 0 NOT NULL,
|
donation_cents INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
|
||||||
|
-- Stripe transaction metadata (for validation and audit)
|
||||||
|
stripe_payment_intent_id VARCHAR,
|
||||||
|
stripe_charge_id VARCHAR,
|
||||||
|
stripe_invoice_id VARCHAR,
|
||||||
|
payment_completed_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
card_last4 VARCHAR(4),
|
||||||
|
card_brand VARCHAR(20),
|
||||||
|
stripe_receipt_url VARCHAR,
|
||||||
|
|
||||||
-- Manual Payment Support
|
-- Manual Payment Support
|
||||||
manual_payment BOOLEAN DEFAULT FALSE NOT NULL,
|
manual_payment BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
manual_payment_notes TEXT,
|
manual_payment_notes TEXT,
|
||||||
@@ -319,6 +396,14 @@ CREATE TABLE IF NOT EXISTS donations (
|
|||||||
stripe_payment_intent_id VARCHAR,
|
stripe_payment_intent_id VARCHAR,
|
||||||
payment_method VARCHAR,
|
payment_method VARCHAR,
|
||||||
|
|
||||||
|
-- Stripe transaction metadata (for validation and audit)
|
||||||
|
stripe_charge_id VARCHAR,
|
||||||
|
stripe_customer_id VARCHAR,
|
||||||
|
payment_completed_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
card_last4 VARCHAR(4),
|
||||||
|
card_brand VARCHAR(20),
|
||||||
|
stripe_receipt_url VARCHAR,
|
||||||
|
|
||||||
-- Metadata
|
-- Metadata
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -445,7 +530,7 @@ CREATE TABLE IF NOT EXISTS storage_usage (
|
|||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
total_bytes_used BIGINT NOT NULL DEFAULT 0,
|
total_bytes_used BIGINT NOT NULL DEFAULT 0,
|
||||||
max_bytes_allowed BIGINT NOT NULL DEFAULT 10737418240, -- 10GB
|
max_bytes_allowed BIGINT NOT NULL DEFAULT 1073741824, -- 1GB
|
||||||
last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -466,29 +551,10 @@ CREATE TABLE IF NOT EXISTS user_invitations (
|
|||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Import Jobs table
|
-- Add FK constraints to import_jobs (now that users table exists)
|
||||||
CREATE TABLE IF NOT EXISTS import_jobs (
|
ALTER TABLE import_jobs
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
ADD CONSTRAINT fk_import_jobs_rollback_by FOREIGN KEY (rollback_by) REFERENCES users(id),
|
||||||
|
ADD CONSTRAINT fk_import_jobs_started_by FOREIGN KEY (started_by) REFERENCES users(id);
|
||||||
filename VARCHAR NOT NULL,
|
|
||||||
status importjobstatus NOT NULL DEFAULT 'processing',
|
|
||||||
total_rows INTEGER DEFAULT 0,
|
|
||||||
processed_rows INTEGER DEFAULT 0,
|
|
||||||
success_count INTEGER DEFAULT 0,
|
|
||||||
error_count INTEGER DEFAULT 0,
|
|
||||||
error_log 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),
|
|
||||||
|
|
||||||
started_by UUID REFERENCES users(id),
|
|
||||||
started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
completed_at TIMESTAMP WITH TIME ZONE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Import Rollback Audit table (for tracking rollback operations)
|
-- Import Rollback Audit table (for tracking rollback operations)
|
||||||
CREATE TABLE IF NOT EXISTS import_rollback_audit (
|
CREATE TABLE IF NOT EXISTS import_rollback_audit (
|
||||||
@@ -542,12 +608,18 @@ CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_subscriptions_plan_id ON subscriptions(plan_id);
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_plan_id ON subscriptions(plan_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_subscription_id ON subscriptions(stripe_subscription_id);
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_subscription_id ON subscriptions(stripe_subscription_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_payment_intent ON subscriptions(stripe_payment_intent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_charge_id ON subscriptions(stripe_charge_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_invoice_id ON subscriptions(stripe_invoice_id);
|
||||||
|
|
||||||
-- Donations indexes
|
-- Donations indexes
|
||||||
CREATE INDEX IF NOT EXISTS idx_donation_user ON donations(user_id);
|
CREATE INDEX IF NOT EXISTS idx_donation_user ON donations(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_donation_type ON donations(donation_type);
|
CREATE INDEX IF NOT EXISTS idx_donation_type ON donations(donation_type);
|
||||||
CREATE INDEX IF NOT EXISTS idx_donation_status ON donations(status);
|
CREATE INDEX IF NOT EXISTS idx_donation_status ON donations(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_donation_created ON donations(created_at);
|
CREATE INDEX IF NOT EXISTS idx_donation_created ON donations(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_donation_payment_intent ON donations(stripe_payment_intent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_donation_charge_id ON donations(stripe_charge_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_donation_customer_id ON donations(stripe_customer_id);
|
||||||
|
|
||||||
-- Import Jobs indexes
|
-- Import Jobs indexes
|
||||||
CREATE INDEX IF NOT EXISTS idx_import_jobs_status ON import_jobs(status);
|
CREATE INDEX IF NOT EXISTS idx_import_jobs_status ON import_jobs(status);
|
||||||
@@ -587,7 +659,7 @@ INSERT INTO storage_usage (id, total_bytes_used, max_bytes_allowed, last_updated
|
|||||||
SELECT
|
SELECT
|
||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
0,
|
0,
|
||||||
10737418240, -- 10GB
|
1073741824, -- 1GB
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM storage_usage);
|
WHERE NOT EXISTS (SELECT 1 FROM storage_usage);
|
||||||
|
|
||||||
|
|||||||
60
models.py
60
models.py
@@ -44,6 +44,13 @@ class DonationStatus(enum.Enum):
|
|||||||
completed = "completed"
|
completed = "completed"
|
||||||
failed = "failed"
|
failed = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethodType(enum.Enum):
|
||||||
|
card = "card"
|
||||||
|
cash = "cash"
|
||||||
|
bank_transfer = "bank_transfer"
|
||||||
|
check = "check"
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
@@ -141,6 +148,13 @@ class User(Base):
|
|||||||
role_changed_at = Column(DateTime(timezone=True), nullable=True, comment="Timestamp when role was last changed")
|
role_changed_at = Column(DateTime(timezone=True), nullable=True, comment="Timestamp when role was last changed")
|
||||||
role_changed_by = Column(UUID(as_uuid=True), ForeignKey('users.id', ondelete='SET NULL'), nullable=True, comment="Admin who changed the role")
|
role_changed_by = Column(UUID(as_uuid=True), ForeignKey('users.id', ondelete='SET NULL'), nullable=True, comment="Admin who changed the role")
|
||||||
|
|
||||||
|
# Stripe Customer ID - Centralized for payment method management
|
||||||
|
stripe_customer_id = Column(String, nullable=True, index=True, comment="Stripe Customer ID for payment method management")
|
||||||
|
|
||||||
|
# Dynamic Registration Form - Custom field responses
|
||||||
|
custom_registration_data = Column(JSON, default=dict, nullable=False,
|
||||||
|
comment="Dynamic registration field responses stored as JSON for custom form fields")
|
||||||
|
|
||||||
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||||
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
@@ -150,6 +164,52 @@ class User(Base):
|
|||||||
rsvps = relationship("EventRSVP", back_populates="user")
|
rsvps = relationship("EventRSVP", back_populates="user")
|
||||||
subscriptions = relationship("Subscription", back_populates="user", foreign_keys="Subscription.user_id")
|
subscriptions = relationship("Subscription", back_populates="user", foreign_keys="Subscription.user_id")
|
||||||
role_changer = relationship("User", foreign_keys=[role_changed_by], remote_side="User.id", post_update=True)
|
role_changer = relationship("User", foreign_keys=[role_changed_by], remote_side="User.id", post_update=True)
|
||||||
|
payment_methods = relationship("PaymentMethod", back_populates="user", foreign_keys="PaymentMethod.user_id")
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethod(Base):
|
||||||
|
"""Stored payment methods for users (Stripe or manual records)"""
|
||||||
|
__tablename__ = "payment_methods"
|
||||||
|
|
||||||
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
|
||||||
|
# Stripe payment method reference
|
||||||
|
stripe_payment_method_id = Column(String, nullable=True, unique=True, index=True, comment="Stripe pm_xxx reference")
|
||||||
|
|
||||||
|
# Card details (stored for display purposes - PCI compliant)
|
||||||
|
card_brand = Column(String(20), nullable=True, comment="Card brand: visa, mastercard, amex, etc.")
|
||||||
|
card_last4 = Column(String(4), nullable=True, comment="Last 4 digits of card")
|
||||||
|
card_exp_month = Column(Integer, nullable=True, comment="Card expiration month")
|
||||||
|
card_exp_year = Column(Integer, nullable=True, comment="Card expiration year")
|
||||||
|
card_funding = Column(String(20), nullable=True, comment="Card funding type: credit, debit, prepaid")
|
||||||
|
|
||||||
|
# Payment type classification
|
||||||
|
payment_type = Column(SQLEnum(PaymentMethodType), default=PaymentMethodType.card, nullable=False)
|
||||||
|
|
||||||
|
# Status flags
|
||||||
|
is_default = Column(Boolean, default=False, nullable=False, comment="Whether this is the default payment method for auto-renewals")
|
||||||
|
is_active = Column(Boolean, default=True, nullable=False, comment="Soft delete flag - False means removed")
|
||||||
|
is_manual = Column(Boolean, default=False, nullable=False, comment="True for manually recorded methods (cash/check)")
|
||||||
|
|
||||||
|
# Manual payment notes (for cash/check records)
|
||||||
|
manual_notes = Column(Text, nullable=True, comment="Admin notes for manual payment methods")
|
||||||
|
|
||||||
|
# Audit trail
|
||||||
|
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, comment="Admin who added this on behalf of user")
|
||||||
|
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||||
|
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = relationship("User", back_populates="payment_methods", foreign_keys=[user_id])
|
||||||
|
creator = relationship("User", foreign_keys=[created_by])
|
||||||
|
|
||||||
|
# Composite index for efficient queries
|
||||||
|
__table_args__ = (
|
||||||
|
Index('idx_payment_method_user_default', 'user_id', 'is_default'),
|
||||||
|
Index('idx_payment_method_active', 'user_id', 'is_active'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Event(Base):
|
class Event(Base):
|
||||||
__tablename__ = "events"
|
__tablename__ = "events"
|
||||||
|
|||||||
@@ -327,6 +327,38 @@ PERMISSIONS = [
|
|||||||
"module": "gallery"
|
"module": "gallery"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# ========== PAYMENT METHODS MODULE ==========
|
||||||
|
{
|
||||||
|
"code": "payment_methods.view",
|
||||||
|
"name": "View Payment Methods",
|
||||||
|
"description": "View user payment methods (masked)",
|
||||||
|
"module": "payment_methods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "payment_methods.view_sensitive",
|
||||||
|
"name": "View Sensitive Payment Details",
|
||||||
|
"description": "View full payment method details including Stripe IDs (requires password)",
|
||||||
|
"module": "payment_methods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "payment_methods.create",
|
||||||
|
"name": "Create Payment Methods",
|
||||||
|
"description": "Add payment methods on behalf of users",
|
||||||
|
"module": "payment_methods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "payment_methods.delete",
|
||||||
|
"name": "Delete Payment Methods",
|
||||||
|
"description": "Delete user payment methods",
|
||||||
|
"module": "payment_methods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "payment_methods.set_default",
|
||||||
|
"name": "Set Default Payment Method",
|
||||||
|
"description": "Set a user's default payment method",
|
||||||
|
"module": "payment_methods"
|
||||||
|
},
|
||||||
|
|
||||||
# ========== SETTINGS MODULE ==========
|
# ========== SETTINGS MODULE ==========
|
||||||
{
|
{
|
||||||
"code": "settings.view",
|
"code": "settings.view",
|
||||||
@@ -453,6 +485,10 @@ DEFAULT_ROLE_PERMISSIONS = {
|
|||||||
"gallery.edit",
|
"gallery.edit",
|
||||||
"gallery.delete",
|
"gallery.delete",
|
||||||
"gallery.moderate",
|
"gallery.moderate",
|
||||||
|
"payment_methods.view",
|
||||||
|
"payment_methods.create",
|
||||||
|
"payment_methods.delete",
|
||||||
|
"payment_methods.set_default",
|
||||||
"settings.view",
|
"settings.view",
|
||||||
"settings.edit",
|
"settings.edit",
|
||||||
"settings.email_templates",
|
"settings.email_templates",
|
||||||
@@ -460,6 +496,36 @@ DEFAULT_ROLE_PERMISSIONS = {
|
|||||||
"settings.logs",
|
"settings.logs",
|
||||||
],
|
],
|
||||||
|
|
||||||
|
UserRole.finance: [
|
||||||
|
# Finance role has all admin permissions plus sensitive payment access
|
||||||
|
"users.view",
|
||||||
|
"users.export",
|
||||||
|
"events.view",
|
||||||
|
"events.rsvps",
|
||||||
|
"events.calendar_export",
|
||||||
|
"subscriptions.view",
|
||||||
|
"subscriptions.create",
|
||||||
|
"subscriptions.edit",
|
||||||
|
"subscriptions.cancel",
|
||||||
|
"subscriptions.activate",
|
||||||
|
"subscriptions.plans",
|
||||||
|
"financials.view",
|
||||||
|
"financials.create",
|
||||||
|
"financials.edit",
|
||||||
|
"financials.delete",
|
||||||
|
"financials.export",
|
||||||
|
"financials.payments",
|
||||||
|
"newsletters.view",
|
||||||
|
"bylaws.view",
|
||||||
|
"gallery.view",
|
||||||
|
"payment_methods.view",
|
||||||
|
"payment_methods.view_sensitive", # Finance can view sensitive payment details
|
||||||
|
"payment_methods.create",
|
||||||
|
"payment_methods.delete",
|
||||||
|
"payment_methods.set_default",
|
||||||
|
"settings.view",
|
||||||
|
],
|
||||||
|
|
||||||
# Superadmin gets all permissions automatically in code,
|
# Superadmin gets all permissions automatically in code,
|
||||||
# so we don't need to explicitly assign them
|
# so we don't need to explicitly assign them
|
||||||
UserRole.superadmin: []
|
UserRole.superadmin: []
|
||||||
|
|||||||
@@ -35,6 +35,21 @@ class R2Storage:
|
|||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx']
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Branding assets (logo and favicon)
|
||||||
|
ALLOWED_BRANDING_TYPES = {
|
||||||
|
'image/jpeg': ['.jpg', '.jpeg'],
|
||||||
|
'image/png': ['.png'],
|
||||||
|
'image/webp': ['.webp'],
|
||||||
|
'image/svg+xml': ['.svg']
|
||||||
|
}
|
||||||
|
|
||||||
|
ALLOWED_FAVICON_TYPES = {
|
||||||
|
'image/x-icon': ['.ico'],
|
||||||
|
'image/vnd.microsoft.icon': ['.ico'],
|
||||||
|
'image/png': ['.png'],
|
||||||
|
'image/svg+xml': ['.svg']
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize R2 client with credentials from environment"""
|
"""Initialize R2 client with credentials from environment"""
|
||||||
self.account_id = os.getenv('R2_ACCOUNT_ID')
|
self.account_id = os.getenv('R2_ACCOUNT_ID')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
Permission Seeding Script for Dynamic RBAC System
|
Permission Seeding Script for Dynamic RBAC System
|
||||||
|
|
||||||
This script populates the database with 59 granular permissions and assigns them
|
This script populates the database with 65 granular permissions and assigns them
|
||||||
to the appropriate dynamic roles (not the old enum roles).
|
to the appropriate dynamic roles (not the old enum roles).
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@@ -33,7 +33,7 @@ engine = create_engine(DATABASE_URL)
|
|||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Permission Definitions (59 permissions across 10 modules)
|
# Permission Definitions (65 permissions across 11 modules)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
PERMISSIONS = [
|
PERMISSIONS = [
|
||||||
@@ -116,6 +116,55 @@ PERMISSIONS = [
|
|||||||
{"code": "permissions.assign", "name": "Assign Permissions", "description": "Assign permissions to roles", "module": "permissions"},
|
{"code": "permissions.assign", "name": "Assign Permissions", "description": "Assign permissions to roles", "module": "permissions"},
|
||||||
{"code": "permissions.manage_roles", "name": "Manage Roles", "description": "Create and manage user roles", "module": "permissions"},
|
{"code": "permissions.manage_roles", "name": "Manage Roles", "description": "Create and manage user roles", "module": "permissions"},
|
||||||
{"code": "permissions.audit", "name": "View Permission Audit Log", "description": "View permission change audit logs", "module": "permissions"},
|
{"code": "permissions.audit", "name": "View Permission Audit Log", "description": "View permission change audit logs", "module": "permissions"},
|
||||||
|
|
||||||
|
# ========== PAYMENT METHODS MODULE (5) ==========
|
||||||
|
{"code": "payment_methods.view", "name": "View Payment Methods", "description": "View user payment methods (masked)", "module": "payment_methods"},
|
||||||
|
{"code": "payment_methods.view_sensitive", "name": "View Sensitive Payment Details", "description": "View full Stripe payment method IDs (requires password)", "module": "payment_methods"},
|
||||||
|
{"code": "payment_methods.create", "name": "Create Payment Methods", "description": "Add payment methods on behalf of users", "module": "payment_methods"},
|
||||||
|
{"code": "payment_methods.delete", "name": "Delete Payment Methods", "description": "Remove user payment methods", "module": "payment_methods"},
|
||||||
|
{"code": "payment_methods.set_default", "name": "Set Default Payment Method", "description": "Set default payment method for users", "module": "payment_methods"},
|
||||||
|
|
||||||
|
# ========== REGISTRATION MODULE (2) ==========
|
||||||
|
{"code": "registration.view", "name": "View Registration Settings", "description": "View registration form schema and settings", "module": "registration"},
|
||||||
|
{"code": "registration.manage", "name": "Manage Registration Form", "description": "Edit registration form schema, steps, and fields", "module": "registration"},
|
||||||
|
|
||||||
|
# ========== DIRECTORY MODULE (2) ==========
|
||||||
|
{"code": "directory.view", "name": "View Directory Settings", "description": "View member directory field configuration", "module": "directory"},
|
||||||
|
{"code": "directory.manage", "name": "Manage Directory Fields", "description": "Enable/disable directory fields shown in Profile and Directory pages", "module": "directory"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Default system roles that must exist
|
||||||
|
DEFAULT_ROLES = [
|
||||||
|
{
|
||||||
|
"code": "guest",
|
||||||
|
"name": "Guest",
|
||||||
|
"description": "Default role for new registrations with no special permissions",
|
||||||
|
"is_system_role": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "member",
|
||||||
|
"name": "Member",
|
||||||
|
"description": "Active paying members with access to member-only content",
|
||||||
|
"is_system_role": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "finance",
|
||||||
|
"name": "Finance",
|
||||||
|
"description": "Financial management role with access to payments, subscriptions, and reports",
|
||||||
|
"is_system_role": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "admin",
|
||||||
|
"name": "Admin",
|
||||||
|
"description": "Board members with full management access except RBAC",
|
||||||
|
"is_system_role": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "superadmin",
|
||||||
|
"name": "Superadmin",
|
||||||
|
"description": "Full system access including RBAC management",
|
||||||
|
"is_system_role": True
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Default permission assignments for dynamic roles
|
# Default permission assignments for dynamic roles
|
||||||
@@ -136,6 +185,9 @@ DEFAULT_ROLE_PERMISSIONS = {
|
|||||||
"subscriptions.cancel", "subscriptions.activate", "subscriptions.plans",
|
"subscriptions.cancel", "subscriptions.activate", "subscriptions.plans",
|
||||||
"subscriptions.export",
|
"subscriptions.export",
|
||||||
"donations.view", "donations.export",
|
"donations.view", "donations.export",
|
||||||
|
# Payment methods - finance can view sensitive details
|
||||||
|
"payment_methods.view", "payment_methods.view_sensitive",
|
||||||
|
"payment_methods.create", "payment_methods.delete", "payment_methods.set_default",
|
||||||
],
|
],
|
||||||
|
|
||||||
"admin": [
|
"admin": [
|
||||||
@@ -157,6 +209,13 @@ DEFAULT_ROLE_PERMISSIONS = {
|
|||||||
"gallery.view", "gallery.upload", "gallery.edit", "gallery.delete", "gallery.moderate",
|
"gallery.view", "gallery.upload", "gallery.edit", "gallery.delete", "gallery.moderate",
|
||||||
"settings.view", "settings.edit", "settings.email_templates", "settings.storage",
|
"settings.view", "settings.edit", "settings.email_templates", "settings.storage",
|
||||||
"settings.logs",
|
"settings.logs",
|
||||||
|
# Payment methods - admin can manage but not view sensitive details
|
||||||
|
"payment_methods.view", "payment_methods.create",
|
||||||
|
"payment_methods.delete", "payment_methods.set_default",
|
||||||
|
# Registration form management
|
||||||
|
"registration.view", "registration.manage",
|
||||||
|
# Directory configuration
|
||||||
|
"directory.view", "directory.manage",
|
||||||
],
|
],
|
||||||
|
|
||||||
"superadmin": [
|
"superadmin": [
|
||||||
@@ -196,7 +255,34 @@ def seed_permissions():
|
|||||||
print(f"\n⚠️ WARNING: Tables not fully cleared! Stopping.")
|
print(f"\n⚠️ WARNING: Tables not fully cleared! Stopping.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Step 2: Create permissions
|
# Step 2: Create default system roles
|
||||||
|
print(f"\n👤 Creating {len(DEFAULT_ROLES)} system roles...")
|
||||||
|
role_map = {}
|
||||||
|
|
||||||
|
for role_data in DEFAULT_ROLES:
|
||||||
|
# Check if role already exists
|
||||||
|
existing_role = db.query(Role).filter(Role.code == role_data["code"]).first()
|
||||||
|
if existing_role:
|
||||||
|
print(f" • {role_data['name']}: Already exists, updating...")
|
||||||
|
existing_role.name = role_data["name"]
|
||||||
|
existing_role.description = role_data["description"]
|
||||||
|
existing_role.is_system_role = role_data["is_system_role"]
|
||||||
|
role_map[role_data["code"]] = existing_role
|
||||||
|
else:
|
||||||
|
print(f" • {role_data['name']}: Creating...")
|
||||||
|
role = Role(
|
||||||
|
code=role_data["code"],
|
||||||
|
name=role_data["name"],
|
||||||
|
description=role_data["description"],
|
||||||
|
is_system_role=role_data["is_system_role"]
|
||||||
|
)
|
||||||
|
db.add(role)
|
||||||
|
role_map[role_data["code"]] = role
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
print(f"✓ Created/updated {len(DEFAULT_ROLES)} system roles")
|
||||||
|
|
||||||
|
# Step 3: Create permissions
|
||||||
print(f"\n📝 Creating {len(PERMISSIONS)} permissions...")
|
print(f"\n📝 Creating {len(PERMISSIONS)} permissions...")
|
||||||
permission_map = {} # Map code to permission object
|
permission_map = {} # Map code to permission object
|
||||||
|
|
||||||
@@ -213,13 +299,13 @@ def seed_permissions():
|
|||||||
db.commit()
|
db.commit()
|
||||||
print(f"✓ Created {len(PERMISSIONS)} permissions")
|
print(f"✓ Created {len(PERMISSIONS)} permissions")
|
||||||
|
|
||||||
# Step 3: Get all roles from database
|
# Step 4: Verify roles exist
|
||||||
print("\n🔍 Fetching dynamic roles...")
|
print("\n🔍 Verifying dynamic roles...")
|
||||||
roles = db.query(Role).all()
|
roles = db.query(Role).all()
|
||||||
role_map = {role.code: role for role in roles}
|
role_map = {role.code: role for role in roles}
|
||||||
print(f"✓ Found {len(roles)} roles: {', '.join(role_map.keys())}")
|
print(f"✓ Found {len(roles)} roles: {', '.join(role_map.keys())}")
|
||||||
|
|
||||||
# Step 4: Assign permissions to roles
|
# Step 5: Assign permissions to roles
|
||||||
print("\n🔐 Assigning permissions to roles...")
|
print("\n🔐 Assigning permissions to roles...")
|
||||||
|
|
||||||
from models import UserRole # Import for enum mapping
|
from models import UserRole # Import for enum mapping
|
||||||
@@ -258,7 +344,7 @@ def seed_permissions():
|
|||||||
db.commit()
|
db.commit()
|
||||||
print(f" ✓ {role.name}: Assigned {len(permission_codes)} permissions")
|
print(f" ✓ {role.name}: Assigned {len(permission_codes)} permissions")
|
||||||
|
|
||||||
# Step 5: Summary
|
# Step 6: Summary
|
||||||
print("\n" + "=" * 80)
|
print("\n" + "=" * 80)
|
||||||
print("📊 SEEDING SUMMARY")
|
print("📊 SEEDING SUMMARY")
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
@@ -273,7 +359,8 @@ def seed_permissions():
|
|||||||
for module, count in sorted(modules.items()):
|
for module, count in sorted(modules.items()):
|
||||||
print(f" • {module.capitalize()}: {count} permissions")
|
print(f" • {module.capitalize()}: {count} permissions")
|
||||||
|
|
||||||
print(f"\nTotal permissions created: {len(PERMISSIONS)}")
|
print(f"\nTotal system roles created: {len(DEFAULT_ROLES)}")
|
||||||
|
print(f"Total permissions created: {len(PERMISSIONS)}")
|
||||||
print(f"Total role-permission mappings: {total_assigned}")
|
print(f"Total role-permission mappings: {total_assigned}")
|
||||||
print("\n✅ Permission seeding completed successfully!")
|
print("\n✅ Permission seeding completed successfully!")
|
||||||
print("\nNext step: Restart backend server")
|
print("\nNext step: Restart backend server")
|
||||||
|
|||||||
Reference in New Issue
Block a user