Fix backend: add passlib/bcrypt, fix datetime timezone issues
This commit is contained in:
148
backend/alembic/versions/7e00fa3c75c9_fix_datetime_timezone.py
Normal file
148
backend/alembic/versions/7e00fa3c75c9_fix_datetime_timezone.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Fix datetime timezone
|
||||
|
||||
Revision ID: 7e00fa3c75c9
|
||||
Revises: 001
|
||||
Create Date: 2026-01-23 11:51:47.640123
|
||||
|
||||
"""
|
||||
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 = '7e00fa3c75c9'
|
||||
down_revision: Union[str, None] = '001'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('attachments', 'uploaded_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('sessions', 'started_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('sessions', 'exported',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('false'))
|
||||
op.drop_index('idx_sessions_dates', table_name='sessions')
|
||||
op.drop_index('idx_sessions_tree', table_name='sessions')
|
||||
op.drop_index('idx_sessions_user', table_name='sessions')
|
||||
op.create_index(op.f('ix_sessions_completed_at'), 'sessions', ['completed_at'], unique=False)
|
||||
op.create_index(op.f('ix_sessions_started_at'), 'sessions', ['started_at'], unique=False)
|
||||
op.create_index(op.f('ix_sessions_tree_id'), 'sessions', ['tree_id'], unique=False)
|
||||
op.create_index(op.f('ix_sessions_user_id'), 'sessions', ['user_id'], unique=False)
|
||||
op.alter_column('teams', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('trees', 'is_active',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('true'))
|
||||
op.alter_column('trees', 'version',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('1'))
|
||||
op.alter_column('trees', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('trees', 'updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('trees', 'usage_count',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('0'))
|
||||
op.drop_index('idx_trees_category', table_name='trees')
|
||||
op.drop_index('idx_trees_search', table_name='trees', postgresql_using='gin')
|
||||
op.drop_index('idx_trees_team', table_name='trees')
|
||||
op.create_index(op.f('ix_trees_category'), 'trees', ['category'], unique=False)
|
||||
op.create_index(op.f('ix_trees_team_id'), 'trees', ['team_id'], unique=False)
|
||||
op.alter_column('users', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('users', 'last_login',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True)
|
||||
op.drop_index('idx_users_email', table_name='users')
|
||||
op.drop_constraint('users_email_key', 'users', type_='unique')
|
||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||
op.create_unique_constraint('users_email_key', 'users', ['email'])
|
||||
op.create_index('idx_users_email', 'users', ['email'], unique=True)
|
||||
op.alter_column('users', 'last_login',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True)
|
||||
op.alter_column('users', 'created_at',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.drop_index(op.f('ix_trees_team_id'), table_name='trees')
|
||||
op.drop_index(op.f('ix_trees_category'), table_name='trees')
|
||||
op.create_index('idx_trees_team', 'trees', ['team_id'], unique=False)
|
||||
op.create_index('idx_trees_search', 'trees', [sa.text("to_tsvector('english'::regconfig, (COALESCE(name, ''::character varying)::text || ' '::text) || COALESCE(description, ''::text))")], unique=False, postgresql_using='gin')
|
||||
op.create_index('idx_trees_category', 'trees', ['category'], unique=False)
|
||||
op.alter_column('trees', 'usage_count',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('0'))
|
||||
op.alter_column('trees', 'updated_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('trees', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('trees', 'version',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('1'))
|
||||
op.alter_column('trees', 'is_active',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('true'))
|
||||
op.alter_column('teams', 'created_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.drop_index(op.f('ix_sessions_user_id'), table_name='sessions')
|
||||
op.drop_index(op.f('ix_sessions_tree_id'), table_name='sessions')
|
||||
op.drop_index(op.f('ix_sessions_started_at'), table_name='sessions')
|
||||
op.drop_index(op.f('ix_sessions_completed_at'), table_name='sessions')
|
||||
op.create_index('idx_sessions_user', 'sessions', ['user_id'], unique=False)
|
||||
op.create_index('idx_sessions_tree', 'sessions', ['tree_id'], unique=False)
|
||||
op.create_index('idx_sessions_dates', 'sessions', ['started_at', 'completed_at'], unique=False)
|
||||
op.alter_column('sessions', 'exported',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('false'))
|
||||
op.alter_column('sessions', 'started_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('attachments', 'uploaded_at',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,5 +1,5 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from sqlalchemy import String, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -18,19 +18,19 @@ class User(Base):
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(50), nullable=False, default="engineer") # admin, engineer, viewer
|
||||
role: Mapped[str] = mapped_column(String(50), nullable=False, default="engineer")
|
||||
team_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("teams.id"),
|
||||
nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=datetime.utcnow
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
team: Mapped[Optional["Team"]] = relationship("Team", back_populates="users")
|
||||
trees: Mapped[list["Tree"]] = relationship("Tree", back_populates="author")
|
||||
sessions: Mapped[list["Session"]] = relationship("Session", back_populates="user")
|
||||
sessions: Mapped[list["Session"]] = relationship("Session", back_populates="user")
|
||||
30
backend/requirements-windows.txt
Normal file
30
backend/requirements-windows.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
# FastAPI and dependencies
|
||||
fastapi==0.109.2
|
||||
uvicorn[standard]==0.27.1
|
||||
python-multipart==0.0.9
|
||||
|
||||
# Pydantic with pre-built wheels
|
||||
pydantic==2.6.1
|
||||
pydantic-settings==2.1.0
|
||||
pydantic-core==2.16.2
|
||||
annotated-types==0.6.0
|
||||
|
||||
# Database
|
||||
sqlalchemy[asyncio]==2.0.27
|
||||
asyncpg==0.29.0
|
||||
alembic==1.13.1
|
||||
|
||||
# Authentication
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
bcrypt==4.1.2
|
||||
|
||||
# Security
|
||||
cryptography==42.0.2
|
||||
|
||||
# Email validation
|
||||
email-validator==2.1.0.post1
|
||||
|
||||
# Others
|
||||
starlette==0.36.3
|
||||
typing-extensions==4.9.0
|
||||
Reference in New Issue
Block a user