Skip to content
Closed
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
10 changes: 5 additions & 5 deletions .env

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file change should be removed from the commit.

Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ STACK_NAME=full-stack-fastapi-project

# Backend
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
SECRET_KEY=changethis
FIRST_SUPERUSER=admin@example.com
FIRST_SUPERUSER_PASSWORD=changethis
SECRET_KEY=kjadhflkafaskjdfhakjfsd
FIRST_SUPERUSER=marcelomizuno@me.com
FIRST_SUPERUSER_PASSWORD=admin123

# Emails
SMTP_HOST=
Expand All @@ -32,11 +32,11 @@ SMTP_SSL=False
SMTP_PORT=587

# Postgres
POSTGRES_SERVER=localhost
POSTGRES_SERVER=db
POSTGRES_PORT=5432
POSTGRES_DB=app
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changethis
POSTGRES_PASSWORD=univesp

SENTRY_DSN=

Expand Down
3 changes: 2 additions & 1 deletion backend/Dockerfile

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least the change with the alembic upgrade head should be mentioned.

I don't think this is something anybody wants on production that happens automatically.

Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ COPY ./app /app/app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync

CMD ["fastapi", "run", "--workers", "4", "app/main.py"]
# Run migrations before starting the server
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Add category to ticket table
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes


# revision identifiers, used by Alembic.
revision = 'b54d6e812a9c'
down_revision = 'f23a9c45d178'
branch_labels = None
depends_on = None


def upgrade():
# Add category column to ticket table
op.add_column('ticket', sa.Column('category', sqlmodel.sql.sqltypes.AutoString(), nullable=False,
server_default="Suporte")) # Default to "Suporte" for existing tickets


def downgrade():
# Remove category column from ticket table
op.drop_column('ticket', 'category')
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Add ticket and comment tables
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects.postgresql import UUID
from uuid import uuid4


# revision identifiers, used by Alembic.
revision = 'f23a9c45d178'
down_revision = '1a31ce608336'
branch_labels = None
depends_on = None


def upgrade():
# Create ticket table
op.create_table(
'ticket',
sa.Column("id", UUID(), nullable=False, server_default=sa.text("gen_random_uuid()")),
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("priority", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("owner_id", UUID(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(
["owner_id"], ["user.id"], ondelete="CASCADE"
),
)

# Create index for ticket lookup by owner
op.create_index(op.f('ix_ticket_owner_id'), 'ticket', ['owner_id'], unique=False)

# Create comment table
op.create_table(
'comment',
sa.Column("id", UUID(), nullable=False, server_default=sa.text("gen_random_uuid()")),
sa.Column("content", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("now()")),
sa.Column("ticket_id", UUID(), nullable=False),
sa.Column("user_id", UUID(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(
["ticket_id"], ["ticket.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(
["user_id"], ["user.id"], ondelete="CASCADE"
),
)

# Create indexes for faster comment lookups
op.create_index(op.f('ix_comment_ticket_id'), 'comment', ['ticket_id'], unique=False)
op.create_index(op.f('ix_comment_user_id'), 'comment', ['user_id'], unique=False)


def downgrade():
# Drop tables in reverse order (comments first, then tickets)
op.drop_index(op.f('ix_comment_user_id'), table_name='comment')
op.drop_index(op.f('ix_comment_ticket_id'), table_name='comment')
op.drop_table('comment')

op.drop_index(op.f('ix_ticket_owner_id'), table_name='ticket')
op.drop_table('ticket')
3 changes: 2 additions & 1 deletion backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from fastapi import APIRouter

from app.api.routes import items, login, private, users, utils
from app.api.routes import items, login, private, users, utils, tickets
from app.core.config import settings

api_router = APIRouter()
api_router.include_router(login.router)
api_router.include_router(users.router)
api_router.include_router(utils.router)
api_router.include_router(items.router)
api_router.include_router(tickets.router) # Add tickets router


if settings.ENVIRONMENT == "local":
Expand Down
172 changes: 172 additions & 0 deletions backend/app/api/routes/tickets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import uuid
from typing import Any

from fastapi import APIRouter, HTTPException, Query
from sqlmodel import func, select

from app.api.deps import CurrentUser, SessionDep
from app.models import (
Ticket,
TicketCreate,
TicketUpdate,
TicketPublic,
TicketsPublic,
TicketDetailPublic,
Comment,
CommentCreate,
CommentPublic,
Message,
TicketCategory,
TicketPriority,
TicketStatus
)

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


@router.get("/", response_model=TicketsPublic)
def read_tickets(
session: SessionDep,
current_user: CurrentUser,
skip: int = 0,
limit: int = 100,
page: int = Query(1, ge=1),
category: TicketCategory = None,
priority: TicketPriority = None,
status: TicketStatus = None
) -> Any:
"""
Listar todos os tickets (com filtros e paginação).
"""
skip = (page - 1) * limit

# Base query
query = select(Ticket)
count_query = select(func.count()).select_from(Ticket)

# Apply filters
if category:
query = query.where(Ticket.category == category)
count_query = count_query.where(Ticket.category == category)

if priority:
query = query.where(Ticket.priority == priority)
count_query = count_query.where(Ticket.priority == priority)

if status:
query = query.where(Ticket.status == status)
count_query = count_query.where(Ticket.status == status)

# Apply user filter if not superuser
if not current_user.is_superuser:
query = query.where(Ticket.user_id == current_user.id)
count_query = count_query.where(Ticket.user_id == current_user.id)

# Get count and tickets
count = session.exec(count_query).one()
tickets = session.exec(query.offset(skip).limit(limit)).all()

return TicketsPublic(data=tickets, count=count, page=page)


@router.get("/{id}", response_model=TicketDetailPublic)
def read_ticket(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
"""
Retorna detalhes de um ticket.
"""
ticket = session.get(Ticket, id)
if not ticket:
raise HTTPException(status_code=404, detail="Ticket não encontrado")

if not current_user.is_superuser and (ticket.user_id != current_user.id):
raise HTTPException(status_code=400, detail="Permissões insuficientes")

return ticket


@router.post("/", response_model=TicketPublic)
def create_ticket(
*, session: SessionDep, current_user: CurrentUser, ticket_in: TicketCreate
) -> Any:
"""
Criar um novo ticket.
"""
ticket = Ticket.model_validate(ticket_in, update={"user_id": current_user.id})
session.add(ticket)
session.commit()
session.refresh(ticket)
return ticket


@router.put("/{id}", response_model=TicketPublic)
def update_ticket(
*,
session: SessionDep,
current_user: CurrentUser,
id: uuid.UUID,
ticket_in: TicketUpdate,
) -> Any:
"""
Atualizar um ticket existente.
"""
ticket = session.get(Ticket, id)
if not ticket:
raise HTTPException(status_code=404, detail="Ticket não encontrado")

if not current_user.is_superuser and (ticket.user_id != current_user.id):
raise HTTPException(status_code=400, detail="Permissões insuficientes")

update_dict = ticket_in.model_dump(exclude_unset=True)
ticket.sqlmodel_update(update_dict)
ticket.updated_at = func.now() # Update the updated_at field

session.add(ticket)
session.commit()
session.refresh(ticket)
return ticket


@router.delete("/{id}")
def delete_ticket(
session: SessionDep, current_user: CurrentUser, id: uuid.UUID
) -> Message:
"""
Deletar um ticket.
"""
ticket = session.get(Ticket, id)
if not ticket:
raise HTTPException(status_code=404, detail="Ticket não encontrado")

if not current_user.is_superuser and (ticket.user_id != current_user.id):
raise HTTPException(status_code=400, detail="Permissões insuficientes")

session.delete(ticket)
session.commit()
return Message(message="Ticket removido com sucesso")


@router.post("/{id}/comments", response_model=CommentPublic)
def create_comment(
*,
session: SessionDep,
current_user: CurrentUser,
id: uuid.UUID,
comment_in: CommentCreate,
) -> Any:
"""
Adicionar comentário a um ticket.
"""
ticket = session.get(Ticket, id)
if not ticket:
raise HTTPException(status_code=404, detail="Ticket não encontrado")

comment = Comment(
**comment_in.model_dump(),
ticket_id=id,
user_id=current_user.id
)

session.add(comment)
session.commit()
session.refresh(comment)
return comment
8 changes: 8 additions & 0 deletions backend/app/api/routes/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from app.api.deps import get_current_active_superuser
from app.models import Message
from app.utils import generate_test_email, send_email
from app.core.config import settings

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

Expand All @@ -29,3 +30,10 @@ def test_email(email_to: EmailStr) -> Message:
@router.get("/health-check/")
async def health_check() -> bool:
return True

@router.get("/cors-origins/")
async def get_cors_origins() -> list[str]:
"""
Get the CORS origins.
"""
return settings.all_cors_origins
Loading
Loading