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/templates/
|
||||||
!.specify/memory/
|
!.specify/memory/
|
||||||
|
|
||||||
.direnv/
|
.direnv/backend/.env
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.boards.repository import BoardRepository
|
from app.boards.repository import BoardRepository
|
||||||
from app.boards.schemas import BoardCreate, BoardDetail, BoardSummary, BoardUpdate, ViewportStateUpdate
|
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
|
from app.database.models.user import User
|
||||||
|
|
||||||
router = APIRouter(prefix="/boards", tags=["boards"])
|
router = APIRouter(prefix="/boards", tags=["boards"])
|
||||||
@@ -18,7 +18,7 @@ router = APIRouter(prefix="/boards", tags=["boards"])
|
|||||||
def create_board(
|
def create_board(
|
||||||
board_data: BoardCreate,
|
board_data: BoardCreate,
|
||||||
current_user: Annotated[User, Depends(get_current_user)],
|
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.
|
Create a new board.
|
||||||
@@ -45,7 +45,7 @@ def create_board(
|
|||||||
@router.get("", response_model=dict)
|
@router.get("", response_model=dict)
|
||||||
def list_boards(
|
def list_boards(
|
||||||
current_user: Annotated[User, Depends(get_current_user)],
|
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,
|
limit: Annotated[int, Query(ge=1, le=100)] = 50,
|
||||||
offset: Annotated[int, Query(ge=0)] = 0,
|
offset: Annotated[int, Query(ge=0)] = 0,
|
||||||
):
|
):
|
||||||
@@ -77,7 +77,7 @@ def list_boards(
|
|||||||
def get_board(
|
def get_board(
|
||||||
board_id: UUID,
|
board_id: UUID,
|
||||||
current_user: Annotated[User, Depends(get_current_user)],
|
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.
|
Get board details by ID.
|
||||||
@@ -111,7 +111,7 @@ def update_board(
|
|||||||
board_id: UUID,
|
board_id: UUID,
|
||||||
board_data: BoardUpdate,
|
board_data: BoardUpdate,
|
||||||
current_user: Annotated[User, Depends(get_current_user)],
|
current_user: Annotated[User, Depends(get_current_user)],
|
||||||
db: Annotated[Session, Depends(get_db)],
|
db: Annotated[Session, Depends(get_db_sync)],
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update board metadata.
|
Update board metadata.
|
||||||
@@ -157,7 +157,7 @@ def update_viewport(
|
|||||||
board_id: UUID,
|
board_id: UUID,
|
||||||
viewport_data: ViewportStateUpdate,
|
viewport_data: ViewportStateUpdate,
|
||||||
current_user: Annotated[User, Depends(get_current_user)],
|
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).
|
Update board viewport state only (optimized for frequent updates).
|
||||||
@@ -198,7 +198,7 @@ def update_viewport(
|
|||||||
def delete_board(
|
def delete_board(
|
||||||
board_id: UUID,
|
board_id: UUID,
|
||||||
current_user: Annotated[User, Depends(get_current_user)],
|
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).
|
Delete a board (soft delete).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Board sharing API endpoints."""
|
"""Board sharing API endpoints."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
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
|
# 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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Share link has expired",
|
detail="Share link has expired",
|
||||||
@@ -69,7 +69,7 @@ def validate_share_link(token: str, db: Session, required_permission: str = "vie
|
|||||||
|
|
||||||
# Update access tracking
|
# Update access tracking
|
||||||
share_link.access_count += 1
|
share_link.access_count += 1
|
||||||
share_link.last_accessed_at = datetime.now(timezone.utc)
|
share_link.last_accessed_at = datetime.now(UTC)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return share_link
|
return share_link
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""JWT token generation and validation."""
|
"""JWT token generation and validation."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from jose import JWTError, jwt
|
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
|
Encoded JWT token string
|
||||||
"""
|
"""
|
||||||
if expires_delta:
|
if expires_delta:
|
||||||
expire = datetime.now(timezone.utc) + expires_delta
|
expire = datetime.now(UTC) + expires_delta
|
||||||
else:
|
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)
|
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -53,12 +53,12 @@ def validate_share_link_token(token: str, db: Session) -> ShareLink | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Check expiration
|
# 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
|
return None
|
||||||
|
|
||||||
# Update access tracking
|
# Update access tracking
|
||||||
share_link.access_count += 1
|
share_link.access_count += 1
|
||||||
share_link.last_accessed_at = datetime.now(timezone.utc)
|
share_link.last_accessed_at = datetime.now(UTC)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return share_link
|
return share_link
|
||||||
|
|||||||
@@ -5,18 +5,16 @@ from uuid import UUID
|
|||||||
|
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
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
|
# For backwards compatibility with synchronous code
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine, select
|
||||||
from sqlalchemy.orm import sessionmaker
|
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.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 for synchronous endpoints
|
||||||
_sync_engine = create_engine(
|
_sync_engine = create_engine(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"""Base model for all database models."""
|
"""Base model for all database models."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime
|
from sqlalchemy import Column, DateTime, func
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ class Base(DeclarativeBase):
|
|||||||
|
|
||||||
# Common columns for all models
|
# Common columns for all models
|
||||||
id: Any = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
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]:
|
def dict(self) -> dict[str, Any]:
|
||||||
"""Convert model to dictionary."""
|
"""Convert model to dictionary."""
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Board database model."""
|
"""Board database model."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID, uuid4
|
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 JSONB
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
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},
|
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(
|
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)
|
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""BoardImage database model - junction table for boards and images."""
|
"""BoardImage database model - junction table for boards and images."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID, uuid4
|
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 JSONB
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
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
|
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(
|
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
|
# Relationships
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""Comment model for board annotations."""
|
"""Comment model for board annotations."""
|
||||||
|
|
||||||
import uuid
|
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.dialects.postgresql import JSONB, UUID
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ class Comment(Base):
|
|||||||
author_name = Column(String(100), nullable=False)
|
author_name = Column(String(100), nullable=False)
|
||||||
content = Column(Text, nullable=False)
|
content = Column(Text, nullable=False)
|
||||||
position = Column(JSONB, nullable=True) # Optional canvas position reference
|
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)
|
is_deleted = Column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Group database model."""
|
"""Group database model."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID, uuid4
|
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.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
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
|
color: Mapped[str] = mapped_column(String(7), nullable=False) # Hex color #RRGGBB
|
||||||
annotation: Mapped[str | None] = mapped_column(Text, nullable=True)
|
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(
|
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
|
# Relationships
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Image database model."""
|
"""Image database model."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID, uuid4
|
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 JSONB
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
@@ -38,7 +38,7 @@ class Image(Base):
|
|||||||
height: Mapped[int] = mapped_column(Integer, nullable=False)
|
height: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
image_metadata: Mapped[dict] = mapped_column(JSONB, 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)
|
reference_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""ShareLink model for board sharing functionality."""
|
"""ShareLink model for board sharing functionality."""
|
||||||
|
|
||||||
import uuid
|
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.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import relationship
|
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)
|
board_id = Column(UUID(as_uuid=True), ForeignKey("boards.id", ondelete="CASCADE"), nullable=False)
|
||||||
token = Column(String(64), unique=True, nullable=False, index=True)
|
token = Column(String(64), unique=True, nullable=False, index=True)
|
||||||
permission_level = Column(String(20), nullable=False) # 'view-only' or 'view-comment'
|
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)
|
expires_at = Column(DateTime, nullable=True)
|
||||||
last_accessed_at = Column(DateTime, nullable=True)
|
last_accessed_at = Column(DateTime, nullable=True)
|
||||||
access_count = Column(Integer, nullable=False, default=0)
|
access_count = Column(Integer, nullable=False, default=0)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""User model for authentication and ownership."""
|
"""User model for authentication and ownership."""
|
||||||
|
|
||||||
import uuid
|
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.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
@@ -18,8 +17,8 @@ class User(Base):
|
|||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
email = Column(String(255), unique=True, nullable=False, index=True)
|
email = Column(String(255), unique=True, nullable=False, index=True)
|
||||||
password_hash = Column(String(255), nullable=False)
|
password_hash = Column(String(255), nullable=False)
|
||||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
||||||
is_active = Column(Boolean, nullable=False, default=True)
|
is_active = Column(Boolean, nullable=False, default=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
|
|||||||
@@ -49,25 +49,17 @@ class ImageRepository:
|
|||||||
result = await self.db.execute(select(Image).where(Image.id == image_id))
|
result = await self.db.execute(select(Image).where(Image.id == image_id))
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
async def get_user_images(
|
async def get_user_images(self, user_id: UUID, limit: int = 50, offset: int = 0) -> tuple[Sequence[Image], int]:
|
||||||
self, user_id: UUID, limit: int = 50, offset: int = 0
|
|
||||||
) -> tuple[Sequence[Image], int]:
|
|
||||||
"""Get all images for a user with pagination."""
|
"""Get all images for a user with pagination."""
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
# Get total count efficiently
|
# Get total count efficiently
|
||||||
count_result = await self.db.execute(
|
count_result = await self.db.execute(select(func.count(Image.id)).where(Image.user_id == user_id))
|
||||||
select(func.count(Image.id)).where(Image.user_id == user_id)
|
|
||||||
)
|
|
||||||
total = count_result.scalar_one()
|
total = count_result.scalar_one()
|
||||||
|
|
||||||
# Get paginated images
|
# Get paginated images
|
||||||
result = await self.db.execute(
|
result = await self.db.execute(
|
||||||
select(Image)
|
select(Image).where(Image.user_id == user_id).order_by(Image.created_at.desc()).limit(limit).offset(offset)
|
||||||
.where(Image.user_id == user_id)
|
|
||||||
.order_by(Image.created_at.desc())
|
|
||||||
.limit(limit)
|
|
||||||
.offset(offset)
|
|
||||||
)
|
)
|
||||||
images = result.scalars().all()
|
images = result.scalars().all()
|
||||||
return images, total
|
return images, total
|
||||||
@@ -126,17 +118,14 @@ class ImageRepository:
|
|||||||
async def get_board_images(self, board_id: UUID) -> Sequence[BoardImage]:
|
async def get_board_images(self, board_id: UUID) -> Sequence[BoardImage]:
|
||||||
"""Get all images for a board, ordered by z-order."""
|
"""Get all images for a board, ordered by z-order."""
|
||||||
result = await self.db.execute(
|
result = await self.db.execute(
|
||||||
select(BoardImage)
|
select(BoardImage).where(BoardImage.board_id == board_id).order_by(BoardImage.z_order.asc())
|
||||||
.where(BoardImage.board_id == board_id)
|
|
||||||
.order_by(BoardImage.z_order.asc())
|
|
||||||
)
|
)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
async def get_board_image(self, board_id: UUID, image_id: UUID) -> BoardImage | None:
|
async def get_board_image(self, board_id: UUID, image_id: UUID) -> BoardImage | None:
|
||||||
"""Get a specific board image."""
|
"""Get a specific board image."""
|
||||||
result = await self.db.execute(
|
result = await self.db.execute(
|
||||||
select(BoardImage)
|
select(BoardImage).where(BoardImage.board_id == board_id, BoardImage.image_id == image_id)
|
||||||
.where(BoardImage.board_id == board_id, BoardImage.image_id == image_id)
|
|
||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
@@ -151,7 +140,7 @@ class ImageRepository:
|
|||||||
) -> BoardImage | None:
|
) -> BoardImage | None:
|
||||||
"""Update board image position, transformations, z-order, or group."""
|
"""Update board image position, transformations, z-order, or group."""
|
||||||
board_image = await self.get_board_image(board_id, image_id)
|
board_image = await self.get_board_image(board_id, image_id)
|
||||||
|
|
||||||
if not board_image:
|
if not board_image:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user