fix part 3
This commit is contained in:
1
.direnv/flake-inputs/92khy67bgrzx85f6052pnw7xrs2jk1v6-source
Symbolic link
1
.direnv/flake-inputs/92khy67bgrzx85f6052pnw7xrs2jk1v6-source
Symbolic link
@@ -0,0 +1 @@
|
||||
/nix/store/92khy67bgrzx85f6052pnw7xrs2jk1v6-source
|
||||
1
.direnv/flake-inputs/lhn3s31zbiq1syclv0rk94bn5g74750c-source
Symbolic link
1
.direnv/flake-inputs/lhn3s31zbiq1syclv0rk94bn5g74750c-source
Symbolic link
@@ -0,0 +1 @@
|
||||
/nix/store/lhn3s31zbiq1syclv0rk94bn5g74750c-source
|
||||
1
.direnv/flake-inputs/xjjq52iwslhz6lbc621a31v0nfdhr5ks-source
Symbolic link
1
.direnv/flake-inputs/xjjq52iwslhz6lbc621a31v0nfdhr5ks-source
Symbolic link
@@ -0,0 +1 @@
|
||||
/nix/store/xjjq52iwslhz6lbc621a31v0nfdhr5ks-source
|
||||
1
.direnv/flake-inputs/zzxxnkdqc6rdycxkylwrs2pg8ahj3cny-source
Symbolic link
1
.direnv/flake-inputs/zzxxnkdqc6rdycxkylwrs2pg8ahj3cny-source
Symbolic link
@@ -0,0 +1 @@
|
||||
/nix/store/zzxxnkdqc6rdycxkylwrs2pg8ahj3cny-source
|
||||
1
.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa
Symbolic link
1
.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa
Symbolic link
@@ -0,0 +1 @@
|
||||
/nix/store/xxizbrvv0ysnp79c429sgsa7g5vwqbr3-nix-shell-env
|
||||
2163
.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc
Normal file
2163
.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc
Normal file
File diff suppressed because one or more lines are too long
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
||||
SECRET_KEY=xM5coyysuo8LZJtNsytgP7OWiKEgHLL75-MGXWzYlxo
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -98,4 +98,4 @@ frontend/dist/
|
||||
!.specify/templates/
|
||||
!.specify/memory/
|
||||
|
||||
.direnv/
|
||||
.direnv/backend/.env
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.boards.repository import BoardRepository
|
||||
from app.boards.schemas import BoardCreate, BoardDetail, BoardSummary, BoardUpdate, ViewportStateUpdate
|
||||
from app.core.deps import get_current_user, get_db
|
||||
from app.core.deps import get_current_user, get_db_sync
|
||||
from app.database.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/boards", tags=["boards"])
|
||||
@@ -18,7 +18,7 @@ router = APIRouter(prefix="/boards", tags=["boards"])
|
||||
def create_board(
|
||||
board_data: BoardCreate,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Create a new board.
|
||||
@@ -45,7 +45,7 @@ def create_board(
|
||||
@router.get("", response_model=dict)
|
||||
def list_boards(
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 50,
|
||||
offset: Annotated[int, Query(ge=0)] = 0,
|
||||
):
|
||||
@@ -77,7 +77,7 @@ def list_boards(
|
||||
def get_board(
|
||||
board_id: UUID,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Get board details by ID.
|
||||
@@ -111,7 +111,7 @@ def update_board(
|
||||
board_id: UUID,
|
||||
board_data: BoardUpdate,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Update board metadata.
|
||||
@@ -157,7 +157,7 @@ def update_viewport(
|
||||
board_id: UUID,
|
||||
viewport_data: ViewportStateUpdate,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Update board viewport state only (optimized for frequent updates).
|
||||
@@ -198,7 +198,7 @@ def update_viewport(
|
||||
def delete_board(
|
||||
board_id: UUID,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Delete a board (soft delete).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Board sharing API endpoints."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
@@ -54,7 +54,7 @@ def validate_share_link(token: str, db: Session, required_permission: str = "vie
|
||||
)
|
||||
|
||||
# Check expiration
|
||||
if share_link.expires_at and share_link.expires_at < datetime.now(timezone.utc):
|
||||
if share_link.expires_at and share_link.expires_at < datetime.now(UTC):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Share link has expired",
|
||||
@@ -69,7 +69,7 @@ def validate_share_link(token: str, db: Session, required_permission: str = "vie
|
||||
|
||||
# Update access tracking
|
||||
share_link.access_count += 1
|
||||
share_link.last_accessed_at = datetime.now(timezone.utc)
|
||||
share_link.last_accessed_at = datetime.now(UTC)
|
||||
db.commit()
|
||||
|
||||
return share_link
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""JWT token generation and validation."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from uuid import UUID
|
||||
|
||||
from jose import JWTError, jwt
|
||||
@@ -21,11 +21,11 @@ def create_access_token(user_id: UUID, email: str, expires_delta: timedelta | No
|
||||
Encoded JWT token string
|
||||
"""
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
expire = datetime.now(UTC) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
expire = datetime.now(UTC) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode = {"sub": str(user_id), "email": email, "exp": expire, "iat": datetime.now(timezone.utc), "type": "access"}
|
||||
to_encode = {"sub": str(user_id), "email": email, "exp": expire, "iat": datetime.now(UTC), "type": "access"}
|
||||
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -53,12 +53,12 @@ def validate_share_link_token(token: str, db: Session) -> ShareLink | None:
|
||||
return None
|
||||
|
||||
# Check expiration
|
||||
if share_link.expires_at and share_link.expires_at < datetime.now(timezone.utc):
|
||||
if share_link.expires_at and share_link.expires_at < datetime.now(UTC):
|
||||
return None
|
||||
|
||||
# Update access tracking
|
||||
share_link.access_count += 1
|
||||
share_link.last_accessed_at = datetime.now(timezone.utc)
|
||||
share_link.last_accessed_at = datetime.now(UTC)
|
||||
db.commit()
|
||||
|
||||
return share_link
|
||||
|
||||
@@ -5,18 +5,16 @@ from uuid import UUID
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.jwt import decode_access_token
|
||||
from app.database.models.user import User
|
||||
from app.database.session import get_db
|
||||
|
||||
# For backwards compatibility with synchronous code
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from app.auth.jwt import decode_access_token
|
||||
from app.core.config import settings
|
||||
from app.database.models.user import User
|
||||
from app.database.session import get_db
|
||||
|
||||
# Sync engine for synchronous endpoints
|
||||
_sync_engine = create_engine(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Base model for all database models."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import Column, DateTime
|
||||
from sqlalchemy import Column, DateTime, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
||||
|
||||
@@ -22,7 +21,7 @@ class Base(DeclarativeBase):
|
||||
|
||||
# Common columns for all models
|
||||
id: Any = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||
created_at: Any = Column(DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
created_at: Any = Column(DateTime, server_default=func.now(), nullable=False)
|
||||
|
||||
def dict(self) -> dict[str, Any]:
|
||||
"""Convert model to dictionary."""
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Board database model."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -42,9 +42,9 @@ class Board(Base):
|
||||
default=lambda: {"x": 0, "y": 0, "zoom": 1.0, "rotation": 0},
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)
|
||||
DateTime, nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""BoardImage database model - junction table for boards and images."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -52,9 +52,9 @@ class BoardImage(Base):
|
||||
PGUUID(as_uuid=True), ForeignKey("groups.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)
|
||||
DateTime, nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Comment model for board annotations."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
@@ -21,7 +20,7 @@ class Comment(Base):
|
||||
author_name = Column(String(100), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
position = Column(JSONB, nullable=True) # Optional canvas position reference
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||
is_deleted = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Group database model."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -33,9 +33,9 @@ class Group(Base):
|
||||
color: Mapped[str] = mapped_column(String(7), nullable=False) # Hex color #RRGGBB
|
||||
annotation: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)
|
||||
DateTime, nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Image database model."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -38,7 +38,7 @@ class Image(Base):
|
||||
height: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
image_metadata: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
|
||||
reference_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""ShareLink model for board sharing functionality."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
@@ -19,7 +18,7 @@ class ShareLink(Base):
|
||||
board_id = Column(UUID(as_uuid=True), ForeignKey("boards.id", ondelete="CASCADE"), nullable=False)
|
||||
token = Column(String(64), unique=True, nullable=False, index=True)
|
||||
permission_level = Column(String(20), nullable=False) # 'view-only' or 'view-comment'
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||
expires_at = Column(DateTime, nullable=True)
|
||||
last_accessed_at = Column(DateTime, nullable=True)
|
||||
access_count = Column(Integer, nullable=False, default=0)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""User model for authentication and ownership."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, String
|
||||
from sqlalchemy import Boolean, Column, DateTime, String, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
@@ -18,8 +17,8 @@ class User(Base):
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
email = Column(String(255), unique=True, nullable=False, index=True)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||
updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -49,25 +49,17 @@ class ImageRepository:
|
||||
result = await self.db.execute(select(Image).where(Image.id == image_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_user_images(
|
||||
self, user_id: UUID, limit: int = 50, offset: int = 0
|
||||
) -> tuple[Sequence[Image], int]:
|
||||
async def get_user_images(self, user_id: UUID, limit: int = 50, offset: int = 0) -> tuple[Sequence[Image], int]:
|
||||
"""Get all images for a user with pagination."""
|
||||
from sqlalchemy import func
|
||||
|
||||
# Get total count efficiently
|
||||
count_result = await self.db.execute(
|
||||
select(func.count(Image.id)).where(Image.user_id == user_id)
|
||||
)
|
||||
count_result = await self.db.execute(select(func.count(Image.id)).where(Image.user_id == user_id))
|
||||
total = count_result.scalar_one()
|
||||
|
||||
# Get paginated images
|
||||
result = await self.db.execute(
|
||||
select(Image)
|
||||
.where(Image.user_id == user_id)
|
||||
.order_by(Image.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
select(Image).where(Image.user_id == user_id).order_by(Image.created_at.desc()).limit(limit).offset(offset)
|
||||
)
|
||||
images = result.scalars().all()
|
||||
return images, total
|
||||
@@ -126,17 +118,14 @@ class ImageRepository:
|
||||
async def get_board_images(self, board_id: UUID) -> Sequence[BoardImage]:
|
||||
"""Get all images for a board, ordered by z-order."""
|
||||
result = await self.db.execute(
|
||||
select(BoardImage)
|
||||
.where(BoardImage.board_id == board_id)
|
||||
.order_by(BoardImage.z_order.asc())
|
||||
select(BoardImage).where(BoardImage.board_id == board_id).order_by(BoardImage.z_order.asc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_board_image(self, board_id: UUID, image_id: UUID) -> BoardImage | None:
|
||||
"""Get a specific board image."""
|
||||
result = await self.db.execute(
|
||||
select(BoardImage)
|
||||
.where(BoardImage.board_id == board_id, BoardImage.image_id == image_id)
|
||||
select(BoardImage).where(BoardImage.board_id == board_id, BoardImage.image_id == image_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user