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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions app/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from fastapi import APIRouter, Cookie, Depends, HTTPException, Response, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.database.models import User
from app.database.session import get_db
from app.schemas.auth import RegisterRequest, UserRead
from app.services.auth import (
InvalidCredentialsError,
UserAlreadyExistsError,
create_session,
get_user_by_session_token,
login_user,
logout_session,
register_user,
)

router = APIRouter(prefix="/api/auth", tags=["auth"])


@router.post("/register", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def register_handler(
user_cred: RegisterRequest, response: Response, db: AsyncSession = Depends(get_db)
):
try:
user = await register_user(db, user_cred.email, user_cred.password)
except UserAlreadyExistsError:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User already exists"
)

session_token = await create_session(db, user)

response.set_cookie(
value=session_token,
key="ackvia_session",
httponly=True,
secure=True,
samesite="lax",
max_age=30 * 24 * 3600,
path="/",
)
return user


@router.post("/login", response_model=UserRead, status_code=status.HTTP_200_OK)
async def login_handler(
user_cred: RegisterRequest, response: Response, db: AsyncSession = Depends(get_db)
):
try:
user, session_token = await login_user(db, user_cred.email, user_cred.password)
except InvalidCredentialsError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Username or password incorrect",
)

response.set_cookie(
value=session_token,
key="ackvia_session",
httponly=True,
secure=True,
samesite="lax",
max_age=30 * 24 * 3600,
path="/",
)
return user


async def get_current_user(
ackvia_session: str | None = Cookie(default=None),
db: AsyncSession = Depends(get_db),
) -> User:
if ackvia_session is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="User is not authenticated"
)
user = await get_user_by_session_token(db, ackvia_session)

if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="User is not authenticated"
)

return user


@router.get("/me", response_model=UserRead, status_code=status.HTTP_200_OK)
async def get_current_user_handler(user: User = Depends(get_current_user)):
return user


@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout_handler(
res: Response,
ackvia_session: str | None = Cookie(default=None),
db: AsyncSession = Depends(get_db),
):
if ackvia_session is not None:
await logout_session(db, ackvia_session)

res.delete_cookie(
key="ackvia_session", httponly=True, secure=True, samesite="lax", path="/"
)
33 changes: 23 additions & 10 deletions app/api/deliveries.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.database.queries.deliveries import get_detailed_delivery_by_id
from app.api.auth import get_current_user
from app.database.models import User
from app.database.queries.deliveries import get_accessible_delivery_by_id
from app.database.session import get_db
from app.schemas.deliveries import DeliveryDetailRead
from app.services.delivery import (
DeliveryNotFoundError,
DeliveryNotReplayableError,
manual_delivery,
queue_manual_replay,
)

router = APIRouter(prefix="/api/deliveries", tags=["deliveries"])
Expand All @@ -17,26 +19,37 @@
async def get_detailed_delivery(
delivery_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
res = await get_detailed_delivery_by_id(db, delivery_id)
res = await get_accessible_delivery_by_id(db, delivery_id, user_id=user.id)

if res is None:
raise HTTPException(
status_code=404,
status_code=status.HTTP_404_NOT_FOUND,
detail="Delivery not found",
)

return res


@router.post(
"/{delivery_id}/replay", response_model=DeliveryDetailRead, status_code=202
"/{delivery_id}/replay",
response_model=DeliveryDetailRead,
status_code=status.HTTP_202_ACCEPTED,
)
async def replay_delivery(delivery_id: int, db: AsyncSession = Depends(get_db)):
async def replay_delivery(
delivery_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
try:
delivery = await manual_delivery(delivery_id, db)
delivery = await queue_manual_replay(db, delivery_id, user.id)
except DeliveryNotFoundError:
raise HTTPException(status_code=404, detail="Delivery not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Delivery not found"
)
except DeliveryNotReplayableError:
raise HTTPException(status_code=409, detail="Delivery is not replayable")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Delivery is not replayable"
)
return delivery
30 changes: 23 additions & 7 deletions app/api/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.auth import get_current_user
from app.core.i18n import SUPPORTED_LANGUAGES, get_locale
from app.database.models import User
from app.database.queries.forms import (
get_accessible_form_by_id,
get_all_forms,
get_form_by_id,
get_form_submissions_from_db,
)
from app.database.session import get_db
from app.exceptions import WorkspaceNotFoundError
from app.schemas.forms import FormRead
from app.schemas.submissions import SubmissionBase
from app.schemas.submissions import SubmissionRead
from app.services.forms import FormCreate, create_form

router = APIRouter(prefix="/api/forms", tags=["forms"])
Expand All @@ -23,24 +26,36 @@ async def create_form_endpoint(
data: FormCreate,
locale: Annotated[tuple[str, dict[str, str]], Depends(get_locale)],
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
current_lang, _ = locale
lang = data.language if data.language in SUPPORTED_LANGUAGES else current_lang

return await create_form(db, data.title, lang, data.destinations)
try:
form = await create_form(
db, data.title, lang, data.destinations, user.id, data.workspace_id
)
return form
except WorkspaceNotFoundError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found"
)


@router.get("/{form_id}/submissions", response_model=list[SubmissionBase])
@router.get("/{form_id}/submissions", response_model=list[SubmissionRead])
async def get_form_submissions(
form_id: uuid.UUID,
limit: int = Query(default=20, ge=1, le=100),
offset: int = Query(default=0, ge=0),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
form = await get_form_by_id(db, form_id)
form = await get_accessible_form_by_id(db, form_id, user.id)

if form is None:
raise HTTPException(status_code=404, detail="Form not foud")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Form not foud"
)

return await get_form_submissions_from_db(
db=db, form_id=form_id, limit=limit, offset=offset
Expand All @@ -52,5 +67,6 @@ async def get_forms(
limit: int = Query(default=20, ge=1, le=100),
offset: int = Query(default=0, ge=0),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
return await get_all_forms(db=db, limit=limit, offset=offset)
return await get_all_forms(db=db, limit=limit, offset=offset, user_id=user.id)
13 changes: 8 additions & 5 deletions app/api/submissions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.database.queries.submissions import get_detailed_submission_by_id
from app.api.auth import get_current_user
from app.database.models import User
from app.database.queries.submissions import get_accessible_submission_by_id
from app.database.session import get_db
from app.schemas.submissions import SubmissionRead

Expand All @@ -12,14 +14,15 @@
async def get_detailed_submission(
sub_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
detailed_submission = await get_detailed_submission_by_id(
db=db, submission_id=sub_id
detailed_submission = await get_accessible_submission_by_id(
db=db, submission_id=sub_id, user_id=user.id
)

if detailed_submission is None:
raise HTTPException(
status_code=404,
status_code=status.HTTP_404_NOT_FOUND,
detail="Submission not found",
)

Expand Down
17 changes: 17 additions & 0 deletions app/api/workspaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from fastapi import APIRouter, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.auth import get_current_user
from app.database.models import User
from app.database.session import get_db
from app.schemas.workspaces import WorkspaceRead
from app.services.workspace import list_user_workspaces

router = APIRouter(prefix="/api/workspaces", tags=["workspaces"])


@router.get("", response_model=list[WorkspaceRead], status_code=status.HTTP_200_OK)
async def get_user_workspaces_handler(
db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)
):
return await list_user_workspaces(db, user)
29 changes: 29 additions & 0 deletions app/database/queries/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.database.models import Session, User


async def get_session_by_token_hash(
db: AsyncSession, token_hash: str
) -> Session | None:
session = await db.execute(
select(Session)
.options(selectinload(Session.user))
.where(Session.token_hash == token_hash)
)
return session.scalar_one_or_none()


async def get_user_by_email(db: AsyncSession, email: str) -> User | None:
return await db.scalar(select(User).where(User.email == email))


async def delete_session_token_by_token_hash(db: AsyncSession, token_hash: str) -> bool:
session = await db.scalar(select(Session).where(Session.token_hash == token_hash))
if session is None:
return False
await db.delete(session)
await db.flush()
return True
29 changes: 26 additions & 3 deletions app/database/queries/deliveries.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
import uuid

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.database.models import Delivery
from app.database.models import Delivery, Form, Submission, WorkspaceMembership


async def get_detailed_delivery_by_id(db: AsyncSession, delivery_id: int):
async def get_accessible_delivery_by_id(
db: AsyncSession, delivery_id: int, user_id: uuid.UUID
):
return await db.scalar(
select(Delivery)
.where(Delivery.id == delivery_id)
.join(Submission, Delivery.submission_id == Submission.id)
.join(Form, Form.id == Submission.form_id)
.join(
WorkspaceMembership, WorkspaceMembership.workspace_id == Form.workspace_id
)
.options(selectinload(Delivery.attempts))
.where(Delivery.id == delivery_id, WorkspaceMembership.user_id == user_id)
)


async def get_delivery_for_processing(
db: AsyncSession, delivery_id: int
) -> Delivery | None:
result = await db.execute(
select(Delivery)
.where(Delivery.id == delivery_id)
.options(
selectinload(Delivery.destination),
selectinload(Delivery.submission).selectinload(Submission.form),
)
)
return result.scalar_one_or_none()
Loading
Loading