chore(quality): apply ruff autofixes and formatting (8 fixes, 18 files reformatted)
This commit is contained in:
+104
-54
@@ -1,66 +1,116 @@
|
||||
"""Authentication routes."""
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import Token, LoginRequest
|
||||
from app.schemas.user import UserCreate, UserResponse
|
||||
from app.config import settings
|
||||
from app.schemas.auth import LoginRequest, Token
|
||||
from app.schemas.user import UserCreate, UserRead
|
||||
from app.utils.security import (
|
||||
verify_password,
|
||||
get_password_hash,
|
||||
create_access_token,
|
||||
decode_access_token,
|
||||
)
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||
router = APIRouter()
|
||||
security = HTTPBearer()
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Verify password."""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""Hash password."""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||
"""Create access token."""
|
||||
from jose import jwt
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
def register(user: UserCreate, db: Session = Depends(get_db)):
|
||||
"""Register new user."""
|
||||
db_user = db.query(User).filter(User.email == user.email).first()
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = User(
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
hashed_password=hashed_password,
|
||||
full_name=user.full_name
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||
"""Login user."""
|
||||
user = db.query(User).filter(User.username == form_data.username).first()
|
||||
if not user or not verify_password(form_data.password, user.hashed_password):
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""Dependency to get the current authenticated user."""
|
||||
token = credentials.credentials
|
||||
payload = decode_access_token(token)
|
||||
if payload is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password"
|
||||
detail="Invalid or expired token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token = create_access_token(
|
||||
data={"sub": str(user.id)},
|
||||
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
user_id = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token payload",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
user = db.query(User).filter(User.id == int(user_id)).first()
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@router.post(
|
||||
"/auth/register", response_model=UserRead, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def register(user_data: UserCreate, db: Session = Depends(get_db)):
|
||||
"""Register a new user."""
|
||||
# Check if email already exists
|
||||
existing_user = db.query(User).filter(User.email == user_data.email).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered"
|
||||
)
|
||||
|
||||
# Create new user
|
||||
hashed_password = get_password_hash(user_data.password)
|
||||
new_user = User(
|
||||
email=user_data.email,
|
||||
password_hash=hashed_password,
|
||||
name=user_data.name,
|
||||
role="member",
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
return new_user
|
||||
|
||||
|
||||
@router.post("/auth/login", response_model=Token)
|
||||
def login(login_data: LoginRequest, db: Session = Depends(get_db)):
|
||||
"""Login and return JWT token."""
|
||||
user = db.query(User).filter(User.email == login_data.email).first()
|
||||
if not user or not verify_password(login_data.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password"
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": str(user.id), "email": user.email})
|
||||
return Token(access_token=access_token)
|
||||
|
||||
|
||||
@router.post("/auth/logout")
|
||||
def logout(current_user: User = Depends(get_current_user)):
|
||||
"""Logout (client should discard the token)."""
|
||||
return {"message": "Logged out successfully"}
|
||||
|
||||
|
||||
@router.get("/auth/me", response_model=UserRead)
|
||||
def get_me(current_user: User = Depends(get_current_user)):
|
||||
"""Get current user info."""
|
||||
return current_user
|
||||
|
||||
|
||||
@router.put("/auth/password")
|
||||
def change_password(
|
||||
current_password: str,
|
||||
new_password: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Change password."""
|
||||
if not verify_password(current_password, current_user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Current password is incorrect",
|
||||
)
|
||||
current_user.password_hash = get_password_hash(new_password)
|
||||
db.commit()
|
||||
return {"message": "Password changed successfully"}
|
||||
|
||||
+210
-31
@@ -1,48 +1,227 @@
|
||||
"""Record routes."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.record import Record
|
||||
from app.schemas.record import RecordCreate, RecordUpdate, RecordResponse
|
||||
from app.models.user import User
|
||||
from app.models.record import Record, CellValue
|
||||
from app.schemas.record import (
|
||||
RecordCreate,
|
||||
RecordRead,
|
||||
RecordUpdate,
|
||||
RecordDetailRead,
|
||||
BulkRecordOperation,
|
||||
)
|
||||
from app.routers.auth import get_current_user
|
||||
from app.routers.tables import check_table_access
|
||||
|
||||
router = APIRouter(prefix="/records", tags=["records"])
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/table/{table_id}", response_model=List[RecordResponse])
|
||||
def list_records(table_id: int, db: Session = Depends(get_db)):
|
||||
"""List all records for a table."""
|
||||
return db.query(Record).filter(Record.column_id.in_(
|
||||
db.query(Column.id).filter(Column.table_id == table_id)
|
||||
)).all()
|
||||
|
||||
@router.post("", response_model=RecordResponse)
|
||||
def create_record(record: RecordCreate, db: Session = Depends(get_db)):
|
||||
"""Create new record."""
|
||||
db_record = Record(**record.model_dump())
|
||||
db.add(db_record)
|
||||
@router.get("/tables/{table_id}/records", response_model=List[RecordDetailRead])
|
||||
def list_records(
|
||||
table_id: int,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List records (paginated)."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
records = (
|
||||
db.query(Record)
|
||||
.filter(Record.table_id == table_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return records
|
||||
|
||||
|
||||
@router.post("/tables/{table_id}/records", response_model=RecordRead, status_code=201)
|
||||
def create_record(
|
||||
table_id: int,
|
||||
data: RecordCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create record."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
record = Record(
|
||||
table_id=table_id, created_by=current_user.id, updated_by=current_user.id
|
||||
)
|
||||
db.add(record)
|
||||
db.flush() # Get record.id
|
||||
|
||||
# Add cell values
|
||||
if data.cells:
|
||||
for cell in data.cells:
|
||||
cell_value = CellValue(
|
||||
record_id=record.id, column_id=cell.column_id, value=cell.value
|
||||
)
|
||||
db.add(cell_value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_record)
|
||||
return db_record
|
||||
db.refresh(record)
|
||||
return record
|
||||
|
||||
@router.put("/{record_id}", response_model=RecordResponse)
|
||||
def update_record(record_id: int, record: RecordUpdate, db: Session = Depends(get_db)):
|
||||
"""Update record."""
|
||||
db_record = db.query(Record).filter(Record.id == record_id).first()
|
||||
if not db_record:
|
||||
|
||||
@router.get("/tables/{table_id}/records/{record_id}", response_model=RecordDetailRead)
|
||||
def get_record(
|
||||
table_id: int,
|
||||
record_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get single record."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
record = (
|
||||
db.query(Record)
|
||||
.filter(Record.id == record_id, Record.table_id == table_id)
|
||||
.first()
|
||||
)
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="Record not found")
|
||||
for key, value in record.model_dump(exclude_unset=True).items():
|
||||
setattr(db_record, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_record)
|
||||
return db_record
|
||||
return record
|
||||
|
||||
@router.delete("/{record_id}")
|
||||
def delete_record(record_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
@router.put("/tables/{table_id}/records/{record_id}", response_model=RecordRead)
|
||||
def update_record(
|
||||
table_id: int,
|
||||
record_id: int,
|
||||
data: RecordUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update record."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
record = (
|
||||
db.query(Record)
|
||||
.filter(Record.id == record_id, Record.table_id == table_id)
|
||||
.first()
|
||||
)
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="Record not found")
|
||||
|
||||
record.updated_by = current_user.id
|
||||
|
||||
# Update cell values
|
||||
if data.cells:
|
||||
for cell in data.cells:
|
||||
existing = (
|
||||
db.query(CellValue)
|
||||
.filter(
|
||||
CellValue.record_id == record_id,
|
||||
CellValue.column_id == cell.column_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
existing.value = cell.value
|
||||
else:
|
||||
cell_value = CellValue(
|
||||
record_id=record_id, column_id=cell.column_id, value=cell.value
|
||||
)
|
||||
db.add(cell_value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
return record
|
||||
|
||||
|
||||
@router.delete("/tables/{table_id}/records/{record_id}")
|
||||
def delete_record(
|
||||
table_id: int,
|
||||
record_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete record."""
|
||||
record = db.query(Record).filter(Record.id == record_id).first()
|
||||
check_table_access(table_id, db, current_user)
|
||||
record = (
|
||||
db.query(Record)
|
||||
.filter(Record.id == record_id, Record.table_id == table_id)
|
||||
.first()
|
||||
)
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="Record not found")
|
||||
db.delete(record)
|
||||
db.commit()
|
||||
return {"message": "Record deleted"}
|
||||
|
||||
|
||||
@router.post("/tables/{table_id}/records/bulk")
|
||||
def bulk_operations(
|
||||
table_id: int,
|
||||
data: BulkRecordOperation,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Bulk create/update records."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
results = {"created": [], "updated": [], "deleted": 0}
|
||||
|
||||
# Create
|
||||
if data.create:
|
||||
for record_data in data.create:
|
||||
record = Record(
|
||||
table_id=table_id,
|
||||
created_by=current_user.id,
|
||||
updated_by=current_user.id,
|
||||
)
|
||||
db.add(record)
|
||||
db.flush()
|
||||
if record_data.cells:
|
||||
for cell in record_data.cells:
|
||||
cell_value = CellValue(
|
||||
record_id=record.id, column_id=cell.column_id, value=cell.value
|
||||
)
|
||||
db.add(cell_value)
|
||||
results["created"].append(record.id)
|
||||
|
||||
# Update
|
||||
if data.update:
|
||||
for update_data in data.update:
|
||||
record_id = update_data.get("id")
|
||||
record = (
|
||||
db.query(Record)
|
||||
.filter(Record.id == record_id, Record.table_id == table_id)
|
||||
.first()
|
||||
)
|
||||
if record:
|
||||
record.updated_by = current_user.id
|
||||
if update_data.get("cells"):
|
||||
for cell in update_data["cells"]:
|
||||
existing = (
|
||||
db.query(CellValue)
|
||||
.filter(
|
||||
CellValue.record_id == record_id,
|
||||
CellValue.column_id == cell["column_id"],
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
existing.value = cell["value"]
|
||||
else:
|
||||
cell_value = CellValue(
|
||||
record_id=record_id,
|
||||
column_id=cell["column_id"],
|
||||
value=cell["value"],
|
||||
)
|
||||
db.add(cell_value)
|
||||
results["updated"].append(record_id)
|
||||
|
||||
# Delete
|
||||
if data.delete:
|
||||
for record_id in data.delete:
|
||||
record = (
|
||||
db.query(Record)
|
||||
.filter(Record.id == record_id, Record.table_id == table_id)
|
||||
.first()
|
||||
)
|
||||
if record:
|
||||
db.delete(record)
|
||||
results["deleted"] += 1
|
||||
|
||||
db.commit()
|
||||
return results
|
||||
|
||||
+147
-46
@@ -1,69 +1,170 @@
|
||||
"""Table routes."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.table import Table
|
||||
from app.models.column import Column
|
||||
from app.schemas.table import TableCreate, TableUpdate, TableResponse
|
||||
from app.schemas.table import (
|
||||
TableCreate,
|
||||
TableRead,
|
||||
TableUpdate,
|
||||
ColumnCreate,
|
||||
ColumnRead,
|
||||
ColumnUpdate,
|
||||
TableDetailRead,
|
||||
)
|
||||
from app.routers.auth import get_current_user
|
||||
from app.routers.workspaces import check_workspace_access
|
||||
|
||||
router = APIRouter(prefix="/tables", tags=["tables"])
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/workspace/{workspace_id}", response_model=List[TableResponse])
|
||||
def list_tables(workspace_id: int, db: Session = Depends(get_db)):
|
||||
"""List all tables in a workspace."""
|
||||
return db.query(Table).filter(Table.workspace_id == workspace_id).all()
|
||||
|
||||
@router.post("", response_model=TableResponse)
|
||||
def create_table(table: TableCreate, db: Session = Depends(get_db)):
|
||||
"""Create new table."""
|
||||
db_table = Table(**table.model_dump())
|
||||
db.add(db_table)
|
||||
db.commit()
|
||||
db.refresh(db_table)
|
||||
return db_table
|
||||
|
||||
@router.get("/{table_id}", response_model=TableResponse)
|
||||
def get_table(table_id: int, db: Session = Depends(get_db)):
|
||||
"""Get table by ID."""
|
||||
def check_table_access(table_id: int, db: Session, user: User) -> Table:
|
||||
"""Check if user has access to table."""
|
||||
table = db.query(Table).filter(Table.id == table_id).first()
|
||||
if not table:
|
||||
raise HTTPException(status_code=404, detail="Table not found")
|
||||
check_workspace_access(table.workspace_id, db, user)
|
||||
return table
|
||||
|
||||
@router.put("/{table_id}", response_model=TableResponse)
|
||||
def update_table(table_id: int, table: TableUpdate, db: Session = Depends(get_db)):
|
||||
"""Update table."""
|
||||
db_table = db.query(Table).filter(Table.id == table_id).first()
|
||||
if not db_table:
|
||||
raise HTTPException(status_code=404, detail="Table not found")
|
||||
for key, value in table.model_dump(exclude_unset=True).items():
|
||||
setattr(db_table, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_table)
|
||||
return db_table
|
||||
|
||||
@router.delete("/{table_id}")
|
||||
def delete_table(table_id: int, db: Session = Depends(get_db)):
|
||||
@router.get("/workspaces/{workspace_id}/tables", response_model=List[TableRead])
|
||||
def list_tables(
|
||||
workspace_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List tables in workspace."""
|
||||
check_workspace_access(workspace_id, db, current_user)
|
||||
return db.query(Table).filter(Table.workspace_id == workspace_id).all()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/workspaces/{workspace_id}/tables", response_model=TableRead, status_code=201
|
||||
)
|
||||
def create_table(
|
||||
workspace_id: int,
|
||||
data: TableCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create table in workspace."""
|
||||
check_workspace_access(workspace_id, db, current_user)
|
||||
table = Table(
|
||||
workspace_id=workspace_id, name=data.name, description=data.description
|
||||
)
|
||||
db.add(table)
|
||||
db.commit()
|
||||
db.refresh(table)
|
||||
return table
|
||||
|
||||
|
||||
@router.get("/tables/{table_id}", response_model=TableDetailRead)
|
||||
def get_table(
|
||||
table_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get table with columns."""
|
||||
table = check_table_access(table_id, db, current_user)
|
||||
return table
|
||||
|
||||
|
||||
@router.put("/tables/{table_id}", response_model=TableRead)
|
||||
def update_table(
|
||||
table_id: int,
|
||||
data: TableUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update table."""
|
||||
table = check_table_access(table_id, db, current_user)
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(table, key, value)
|
||||
db.commit()
|
||||
db.refresh(table)
|
||||
return table
|
||||
|
||||
|
||||
@router.delete("/tables/{table_id}")
|
||||
def delete_table(
|
||||
table_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete table."""
|
||||
table = db.query(Table).filter(Table.id == table_id).first()
|
||||
if not table:
|
||||
raise HTTPException(status_code=404, detail="Table not found")
|
||||
table = check_table_access(table_id, db, current_user)
|
||||
db.delete(table)
|
||||
db.commit()
|
||||
return {"message": "Table deleted"}
|
||||
|
||||
@router.get("/{table_id}/columns")
|
||||
def get_columns(table_id: int, db: Session = Depends(get_db)):
|
||||
"""Get columns for a table."""
|
||||
return db.query(Column).filter(Column.table_id == table_id).all()
|
||||
|
||||
@router.post("/{table_id}/columns")
|
||||
def create_column(table_id: int, column: dict, db: Session = Depends(get_db)):
|
||||
"""Create column in table."""
|
||||
db_column = Column(table_id=table_id, **column)
|
||||
db.add(db_column)
|
||||
@router.post("/tables/{table_id}/columns", response_model=ColumnRead, status_code=201)
|
||||
def add_column(
|
||||
table_id: int,
|
||||
data: ColumnCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Add column to table."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
column = Column(
|
||||
table_id=table_id,
|
||||
name=data.name,
|
||||
type=data.type,
|
||||
required=data.required,
|
||||
default_value=data.default_value,
|
||||
position=data.position,
|
||||
options=data.options,
|
||||
)
|
||||
db.add(column)
|
||||
db.commit()
|
||||
db.refresh(db_column)
|
||||
return db_column
|
||||
db.refresh(column)
|
||||
return column
|
||||
|
||||
|
||||
@router.put("/tables/{table_id}/columns/{column_id}", response_model=ColumnRead)
|
||||
def update_column(
|
||||
table_id: int,
|
||||
column_id: int,
|
||||
data: ColumnUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update column."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
column = (
|
||||
db.query(Column)
|
||||
.filter(Column.id == column_id, Column.table_id == table_id)
|
||||
.first()
|
||||
)
|
||||
if not column:
|
||||
raise HTTPException(status_code=404, detail="Column not found")
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(column, key, value)
|
||||
db.commit()
|
||||
db.refresh(column)
|
||||
return column
|
||||
|
||||
|
||||
@router.delete("/tables/{table_id}/columns/{column_id}")
|
||||
def delete_column(
|
||||
table_id: int,
|
||||
column_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete column."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
column = (
|
||||
db.query(Column)
|
||||
.filter(Column.id == column_id, Column.table_id == table_id)
|
||||
.first()
|
||||
)
|
||||
if not column:
|
||||
raise HTTPException(status_code=404, detail="Column not found")
|
||||
db.delete(column)
|
||||
db.commit()
|
||||
return {"message": "Column deleted"}
|
||||
|
||||
@@ -1,23 +1,67 @@
|
||||
"""User routes."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserResponse, UserUpdate
|
||||
from app.schemas.user import UserRead, UserUpdate
|
||||
from app.routers.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def get_current_user(db: Session = Depends(get_db), current_user: User = Depends(lambda: None)):
|
||||
"""Get current user."""
|
||||
# Placeholder - implement auth dependency
|
||||
return UserResponse(id=1, email="user@example.com", username="user", is_active=1, created_at=None)
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
def get_user(user_id: int, db: Session = Depends(get_db)):
|
||||
"""Get user by ID."""
|
||||
def require_admin(current_user: User = Depends(get_current_user)) -> User:
|
||||
"""Require admin role."""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required"
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/users", response_model=List[UserRead])
|
||||
def list_users(db: Session = Depends(get_db), admin: User = Depends(require_admin)):
|
||||
"""List all users (admin only)."""
|
||||
return db.query(User).all()
|
||||
|
||||
|
||||
@router.get("/users/{user_id}", response_model=UserRead)
|
||||
def get_user(
|
||||
user_id: int, db: Session = Depends(get_db), admin: User = Depends(require_admin)
|
||||
):
|
||||
"""Get user by ID (admin only)."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/users/{user_id}", response_model=UserRead)
|
||||
def update_user(
|
||||
user_id: int,
|
||||
data: UserUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(require_admin),
|
||||
):
|
||||
"""Update user (admin only)."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(user, key, value)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@router.delete("/users/{user_id}")
|
||||
def delete_user(
|
||||
user_id: int, db: Session = Depends(get_db), admin: User = Depends(require_admin)
|
||||
):
|
||||
"""Delete user (admin only)."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
return {"message": "User deleted"}
|
||||
|
||||
@@ -1,54 +1,110 @@
|
||||
"""View routes."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.view import View
|
||||
from app.schemas.view import ViewCreate, ViewUpdate, ViewResponse
|
||||
from app.schemas.view import ViewCreate, ViewRead, ViewUpdate
|
||||
from app.routers.auth import get_current_user
|
||||
from app.routers.tables import check_table_access
|
||||
|
||||
router = APIRouter(prefix="/views", tags=["views"])
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/table/{table_id}", response_model=List[ViewResponse])
|
||||
def list_views(table_id: int, db: Session = Depends(get_db)):
|
||||
"""List all views for a table."""
|
||||
|
||||
@router.get("/tables/{table_id}/views", response_model=List[ViewRead])
|
||||
def list_views(
|
||||
table_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List views for table."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
return db.query(View).filter(View.table_id == table_id).all()
|
||||
|
||||
@router.post("", response_model=ViewResponse)
|
||||
def create_view(view: ViewCreate, db: Session = Depends(get_db)):
|
||||
"""Create new view."""
|
||||
db_view = View(**view.model_dump())
|
||||
db.add(db_view)
|
||||
db.commit()
|
||||
db.refresh(db_view)
|
||||
return db_view
|
||||
|
||||
@router.get("/{view_id}", response_model=ViewResponse)
|
||||
def get_view(view_id: int, db: Session = Depends(get_db)):
|
||||
"""Get view by ID."""
|
||||
@router.post("/tables/{table_id}/views", response_model=ViewRead, status_code=201)
|
||||
def create_view(
|
||||
table_id: int,
|
||||
data: ViewCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create view."""
|
||||
check_table_access(table_id, db, current_user)
|
||||
|
||||
# If is_default, unset other defaults
|
||||
if data.is_default:
|
||||
db.query(View).filter(
|
||||
View.table_id == table_id, View.is_default == True
|
||||
).update({"is_default": False})
|
||||
|
||||
view = View(
|
||||
table_id=table_id,
|
||||
name=data.name,
|
||||
type=data.type,
|
||||
config=data.config,
|
||||
is_default=data.is_default,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.add(view)
|
||||
db.commit()
|
||||
db.refresh(view)
|
||||
return view
|
||||
|
||||
|
||||
@router.get("/views/{view_id}", response_model=ViewRead)
|
||||
def get_view(
|
||||
view_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get view config."""
|
||||
view = db.query(View).filter(View.id == view_id).first()
|
||||
if not view:
|
||||
raise HTTPException(status_code=404, detail="View not found")
|
||||
check_table_access(view.table_id, db, current_user)
|
||||
return view
|
||||
|
||||
@router.put("/{view_id}", response_model=ViewResponse)
|
||||
def update_view(view_id: int, view: ViewUpdate, db: Session = Depends(get_db)):
|
||||
"""Update view."""
|
||||
db_view = db.query(View).filter(View.id == view_id).first()
|
||||
if not db_view:
|
||||
raise HTTPException(status_code=404, detail="View not found")
|
||||
for key, value in view.model_dump(exclude_unset=True).items():
|
||||
setattr(db_view, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_view)
|
||||
return db_view
|
||||
|
||||
@router.delete("/{view_id}")
|
||||
def delete_view(view_id: int, db: Session = Depends(get_db)):
|
||||
@router.put("/views/{view_id}", response_model=ViewRead)
|
||||
def update_view(
|
||||
view_id: int,
|
||||
data: ViewUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update view."""
|
||||
view = db.query(View).filter(View.id == view_id).first()
|
||||
if not view:
|
||||
raise HTTPException(status_code=404, detail="View not found")
|
||||
check_table_access(view.table_id, db, current_user)
|
||||
|
||||
# If setting as default, unset other defaults
|
||||
if data.is_default and not view.is_default:
|
||||
db.query(View).filter(
|
||||
View.table_id == view.table_id, View.is_default == True
|
||||
).update({"is_default": False})
|
||||
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(view, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(view)
|
||||
return view
|
||||
|
||||
|
||||
@router.delete("/views/{view_id}")
|
||||
def delete_view(
|
||||
view_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete view."""
|
||||
view = db.query(View).filter(View.id == view_id).first()
|
||||
if not view:
|
||||
raise HTTPException(status_code=404, detail="View not found")
|
||||
check_table_access(view.table_id, db, current_user)
|
||||
db.delete(view)
|
||||
db.commit()
|
||||
return {"message": "View deleted"}
|
||||
|
||||
@@ -1,54 +1,215 @@
|
||||
"""Workspace routes."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.workspace import Workspace
|
||||
from app.schemas.workspace import WorkspaceCreate, WorkspaceUpdate, WorkspaceResponse
|
||||
from app.models.user import User
|
||||
from app.models.workspace import Workspace, WorkspaceMember
|
||||
from app.schemas.workspace import (
|
||||
WorkspaceCreate,
|
||||
WorkspaceRead,
|
||||
WorkspaceUpdate,
|
||||
WorkspaceMemberCreate,
|
||||
WorkspaceMemberRead,
|
||||
WorkspaceMemberUpdate,
|
||||
)
|
||||
from app.routers.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/workspaces", tags=["workspaces"])
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("", response_model=List[WorkspaceResponse])
|
||||
def list_workspaces(db: Session = Depends(get_db)):
|
||||
"""List all workspaces."""
|
||||
return db.query(Workspace).all()
|
||||
|
||||
@router.post("", response_model=WorkspaceResponse)
|
||||
def create_workspace(workspace: WorkspaceCreate, db: Session = Depends(get_db)):
|
||||
"""Create new workspace."""
|
||||
db_workspace = Workspace(**workspace.model_dump())
|
||||
db.add(db_workspace)
|
||||
db.commit()
|
||||
db.refresh(db_workspace)
|
||||
return db_workspace
|
||||
|
||||
@router.get("/{workspace_id}", response_model=WorkspaceResponse)
|
||||
def get_workspace(workspace_id: int, db: Session = Depends(get_db)):
|
||||
"""Get workspace by ID."""
|
||||
def check_workspace_access(workspace_id: int, db: Session, user: User) -> Workspace:
|
||||
"""Check if user has access to workspace."""
|
||||
workspace = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not workspace:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
# Owner or member has access
|
||||
if workspace.owner_id != user.id:
|
||||
membership = (
|
||||
db.query(WorkspaceMember)
|
||||
.filter(
|
||||
WorkspaceMember.workspace_id == workspace_id,
|
||||
WorkspaceMember.user_id == user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not membership:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
return workspace
|
||||
|
||||
@router.put("/{workspace_id}", response_model=WorkspaceResponse)
|
||||
def update_workspace(workspace_id: int, workspace: WorkspaceUpdate, db: Session = Depends(get_db)):
|
||||
"""Update workspace."""
|
||||
db_workspace = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not db_workspace:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
for key, value in workspace.model_dump(exclude_unset=True).items():
|
||||
setattr(db_workspace, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_workspace)
|
||||
return db_workspace
|
||||
|
||||
@router.delete("/{workspace_id}")
|
||||
def delete_workspace(workspace_id: int, db: Session = Depends(get_db)):
|
||||
"""Delete workspace."""
|
||||
workspace = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not workspace:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
@router.get("/workspaces", response_model=List[WorkspaceRead])
|
||||
def list_workspaces(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""List user's workspaces (owned + member)."""
|
||||
owned = db.query(Workspace).filter(Workspace.owner_id == current_user.id).all()
|
||||
memberships = (
|
||||
db.query(WorkspaceMember)
|
||||
.filter(WorkspaceMember.user_id == current_user.id)
|
||||
.all()
|
||||
)
|
||||
member_ids = [m.workspace_id for m in memberships]
|
||||
member_workspaces = (
|
||||
db.query(Workspace).filter(Workspace.id.in_(member_ids)).all()
|
||||
if member_ids
|
||||
else []
|
||||
)
|
||||
return list(set(owned + member_workspaces))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/workspaces", response_model=WorkspaceRead, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def create_workspace(
|
||||
data: WorkspaceCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new workspace."""
|
||||
workspace = Workspace(
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
owner_id=current_user.id,
|
||||
is_private=data.is_private,
|
||||
)
|
||||
db.add(workspace)
|
||||
db.commit()
|
||||
db.refresh(workspace)
|
||||
return workspace
|
||||
|
||||
|
||||
@router.get("/workspaces/{workspace_id}", response_model=WorkspaceRead)
|
||||
def get_workspace(
|
||||
workspace_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get workspace details."""
|
||||
return check_workspace_access(workspace_id, db, current_user)
|
||||
|
||||
|
||||
@router.put("/workspaces/{workspace_id}", response_model=WorkspaceRead)
|
||||
def update_workspace(
|
||||
workspace_id: int,
|
||||
data: WorkspaceUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update workspace (owner only)."""
|
||||
workspace = check_workspace_access(workspace_id, db, current_user)
|
||||
if workspace.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Owner access required")
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(workspace, key, value)
|
||||
db.commit()
|
||||
db.refresh(workspace)
|
||||
return workspace
|
||||
|
||||
|
||||
@router.delete("/workspaces/{workspace_id}")
|
||||
def delete_workspace(
|
||||
workspace_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete workspace (owner only)."""
|
||||
workspace = check_workspace_access(workspace_id, db, current_user)
|
||||
if workspace.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Owner access required")
|
||||
db.delete(workspace)
|
||||
db.commit()
|
||||
return {"message": "Workspace deleted"}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/workspaces/{workspace_id}/members", response_model=List[WorkspaceMemberRead]
|
||||
)
|
||||
def list_members(
|
||||
workspace_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List workspace members."""
|
||||
check_workspace_access(workspace_id, db, current_user)
|
||||
return (
|
||||
db.query(WorkspaceMember)
|
||||
.filter(WorkspaceMember.workspace_id == workspace_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/workspaces/{workspace_id}/members",
|
||||
response_model=WorkspaceMemberRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def add_member(
|
||||
workspace_id: int,
|
||||
data: WorkspaceMemberCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Add member to workspace (owner only)."""
|
||||
workspace = check_workspace_access(workspace_id, db, current_user)
|
||||
if workspace.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Owner access required")
|
||||
membership = WorkspaceMember(
|
||||
workspace_id=workspace_id, user_id=data.user_id, role=data.role
|
||||
)
|
||||
db.add(membership)
|
||||
db.commit()
|
||||
db.refresh(membership)
|
||||
return membership
|
||||
|
||||
|
||||
@router.put(
|
||||
"/workspaces/{workspace_id}/members/{user_id}", response_model=WorkspaceMemberRead
|
||||
)
|
||||
def update_member(
|
||||
workspace_id: int,
|
||||
user_id: int,
|
||||
data: WorkspaceMemberUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update member role (owner only)."""
|
||||
check_workspace_access(workspace_id, db, current_user)
|
||||
membership = (
|
||||
db.query(WorkspaceMember)
|
||||
.filter(
|
||||
WorkspaceMember.workspace_id == workspace_id,
|
||||
WorkspaceMember.user_id == user_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not membership:
|
||||
raise HTTPException(status_code=404, detail="Member not found")
|
||||
membership.role = data.role
|
||||
db.commit()
|
||||
db.refresh(membership)
|
||||
return membership
|
||||
|
||||
|
||||
@router.delete("/workspaces/{workspace_id}/members/{user_id}")
|
||||
def remove_member(
|
||||
workspace_id: int,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Remove member from workspace (owner only)."""
|
||||
check_workspace_access(workspace_id, db, current_user)
|
||||
membership = (
|
||||
db.query(WorkspaceMember)
|
||||
.filter(
|
||||
WorkspaceMember.workspace_id == workspace_id,
|
||||
WorkspaceMember.user_id == user_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not membership:
|
||||
raise HTTPException(status_code=404, detail="Member not found")
|
||||
db.delete(membership)
|
||||
db.commit()
|
||||
return {"message": "Member removed"}
|
||||
|
||||
Reference in New Issue
Block a user