Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ Thumbs.db
__pycache__/
*.py[cod]
*$py.class

.claude/
54 changes: 54 additions & 0 deletions portal-backend-python/add_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Script to add a user to the admin allowlist.
Usage: python add_admin.py <auth0_id>
"""
import sys
from db import SessionLocal
from models.user import User
from models.admin_user import AdminUser

def add_admin(auth0_id: str):
"""Add a user to the admin allowlist."""
db = SessionLocal()

try:
# Get or create the user
user = db.query(User).filter(User.auth0_id == auth0_id).first()
if not user:
print(f"User with auth0_id '{auth0_id}' not found.")
print("Please submit an application first to create your user account.")
return False

# Check if already an admin
admin_user = db.query(AdminUser).filter(AdminUser.user_id == user.id).first()
if admin_user:
print(f"User {auth0_id} is already an admin!")
return True

# Add to admin allowlist
admin_user = AdminUser(user_id=user.id)
db.add(admin_user)
db.commit()

print(f"✓ Successfully added {auth0_id} to admin allowlist!")
print(f" User ID: {user.id}")
print(f" Admin ID: {admin_user.id}")
return True

except Exception as e:
print(f"Error: {e}")
db.rollback()
return False
finally:
db.close()

if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python add_admin.py <auth0_id>")
print("Example: python add_admin.py google-oauth2|113033060705795781005")
sys.exit(1)

auth0_id = sys.argv[1]
success = add_admin(auth0_id)
sys.exit(0 if success else 1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""create admin_user table and add locking fields to application

Revision ID: c767c615424d
Revises: 9db97cbe2f67
Create Date: 2025-12-12 21:54:11.789680

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'c767c615424d'
down_revision: Union[str, Sequence[str], None] = '9db97cbe2f67'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('admin_user',
sa.Column('id', sa.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('user_id', sa.UUID(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('current_session_id', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id')
)
op.add_column('application', sa.Column('locked_by', sa.UUID(), nullable=True))
op.add_column('application', sa.Column('locked_at', sa.DateTime(), nullable=True))
op.add_column('application', sa.Column('decided_by', sa.UUID(), nullable=True))
op.create_foreign_key(None, 'application', 'user', ['locked_by'], ['id'], ondelete='SET NULL')
op.create_foreign_key(None, 'application', 'user', ['decided_by'], ['id'], ondelete='SET NULL')
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'application', type_='foreignkey')
op.drop_constraint(None, 'application', type_='foreignkey')
op.drop_column('application', 'decided_by')
op.drop_column('application', 'locked_at')
op.drop_column('application', 'locked_by')
op.drop_table('admin_user')
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions portal-backend-python/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def test_session(test_sessionmaker):
any changes and closes the session to ensure test isolation.
"""
session = test_sessionmaker()
# Start a savepoint to ensure we can rollback even with explicit flushes
session.begin_nested()
try:
yield session
finally:
Expand Down
3 changes: 2 additions & 1 deletion portal-backend-python/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .question import Question
from .response import Response
from .application import Application
from .admin_user import AdminUser

# should this be dynamically generated?
__all__ = ["Base", "User", "Form", "Question", "Response", "Application"]
__all__ = ["Base", "User", "Form", "Question", "Response", "Application", "AdminUser"]
23 changes: 23 additions & 0 deletions portal-backend-python/models/admin_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from sqlalchemy import text, Column, ForeignKey, String, DateTime
from sqlalchemy.dialects.postgresql import UUID
from models.base import Base
from sqlalchemy import func


class AdminUser(Base):
__tablename__ = "admin_user"

id = Column(
UUID(as_uuid=True), primary_key=True, server_default=text("uuid_generate_v4()")
)
user_id = Column(
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False, unique=True
)
created_at = Column(
DateTime,
nullable=False,
server_default=func.now(),
)
current_session_id = Column(
String, nullable=True
) # For multi-tab invalidation
9 changes: 9 additions & 0 deletions portal-backend-python/models/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,12 @@ class Application(Base):
submission_json = Column(
JSONB, nullable=True
) # redundant but makes fetching form data easier
locked_by = Column(
UUID(as_uuid=True), ForeignKey("user.id", ondelete="SET NULL"), nullable=True
) # FK to user (admin) who is currently reviewing
locked_at = Column(
DateTime, nullable=True
) # Timestamp when lock was acquired
decided_by = Column(
UUID(as_uuid=True), ForeignKey("user.id", ondelete="SET NULL"), nullable=True
) # FK to admin user who made the decision
Loading