innovationhub-cli 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -86
  3. package/index.js +228 -29
  4. package/package.json +12 -3
  5. package/templates/nest/.env.example +41 -0
  6. package/templates/nest/.prettierrc +4 -0
  7. package/templates/nest/Dockerfile +17 -0
  8. package/templates/nest/README.md +224 -0
  9. package/templates/nest/addons/.github/dependabot.yml +11 -0
  10. package/templates/nest/addons/.github/labeler.yml +34 -0
  11. package/templates/nest/addons/.github/workflows/check-branch.yml +24 -0
  12. package/templates/nest/addons/.github/workflows/ci.yml +29 -0
  13. package/templates/nest/addons/.github/workflows/draft-release.yml +22 -0
  14. package/templates/nest/addons/.github/workflows/pr-labeler.yml +16 -0
  15. package/templates/nest/addons/.github/workflows/release.yml +33 -0
  16. package/templates/nest/addons/.github/workflows/security-audit.yml +17 -0
  17. package/templates/nest/addons/.github/workflows/semantic-pr.yml +31 -0
  18. package/templates/nest/addons/.github/workflows/stale.yml +24 -0
  19. package/templates/nest/addons/.husky/commit-msg +17 -0
  20. package/templates/nest/addons/.husky/pre-commit +1 -0
  21. package/templates/nest/addons/addons.json +17 -0
  22. package/templates/nest/addons/cloudinary/cloudinary.module.ts +11 -0
  23. package/templates/nest/addons/cloudinary/cloudinary.provider.ts +16 -0
  24. package/templates/nest/addons/cloudinary/cloudinary.service.ts +54 -0
  25. package/templates/nest/docker-compose.yml +58 -0
  26. package/templates/nest/eslint.config.mjs +34 -0
  27. package/templates/nest/jest.config.js +17 -0
  28. package/templates/nest/nest-cli.json +12 -0
  29. package/templates/nest/package.json +99 -0
  30. package/templates/nest/src/app.controller.ts +7 -0
  31. package/templates/nest/src/app.module.ts +69 -0
  32. package/templates/nest/src/app.service.ts +4 -0
  33. package/templates/nest/src/auth/auth.controller.ts +97 -0
  34. package/templates/nest/src/auth/auth.module.ts +46 -0
  35. package/templates/nest/src/auth/auth.service.ts +231 -0
  36. package/templates/nest/src/auth/decorators/roles.decorator.ts +5 -0
  37. package/templates/nest/src/auth/dto/change-password.dto.ts +21 -0
  38. package/templates/nest/src/auth/dto/login-response.dto.ts +25 -0
  39. package/templates/nest/src/auth/dto/login.dto.ts +15 -0
  40. package/templates/nest/src/auth/dto/refresh-token.dto.ts +12 -0
  41. package/templates/nest/src/auth/entities/refresh-token.entity.ts +18 -0
  42. package/templates/nest/src/auth/enums/role.enum.ts +4 -0
  43. package/templates/nest/src/auth/guards/jwt-auth.guard.ts +5 -0
  44. package/templates/nest/src/auth/guards/refresh-token.guard.ts +5 -0
  45. package/templates/nest/src/auth/guards/roles.guard.ts +23 -0
  46. package/templates/nest/src/auth/interfaces/jwt-payload.interface.ts +10 -0
  47. package/templates/nest/src/auth/strategies/jwt.strategy.ts +28 -0
  48. package/templates/nest/src/auth/strategies/local.strategy.ts +23 -0
  49. package/templates/nest/src/auth/strategies/refresh-token.strategy.ts +32 -0
  50. package/templates/nest/src/common/base.entity.ts +19 -0
  51. package/templates/nest/src/common/base.repository.ts +79 -0
  52. package/templates/nest/src/common/base.service.ts +28 -0
  53. package/templates/nest/src/common/constants/errors.constants.ts +33 -0
  54. package/templates/nest/src/common/decorators/user.decorator.ts +9 -0
  55. package/templates/nest/src/common/dto/base-query.dto.ts +56 -0
  56. package/templates/nest/src/common/irepository.ts +18 -0
  57. package/templates/nest/src/common/utils/duration.utils.ts +33 -0
  58. package/templates/nest/src/common/utils/pagination.utils.ts +35 -0
  59. package/templates/nest/src/common/utils/slug.utils.ts +14 -0
  60. package/templates/nest/src/common/utils/transform.utils.ts +62 -0
  61. package/templates/nest/src/common/validators/is-date-after.validator.ts +40 -0
  62. package/templates/nest/src/data-source.ts +23 -0
  63. package/templates/nest/src/main.ts +44 -0
  64. package/templates/nest/src/user/dto/create-user.dto.ts +50 -0
  65. package/templates/nest/src/user/dto/query-users.dto.ts +23 -0
  66. package/templates/nest/src/user/dto/update-user.dto.ts +15 -0
  67. package/templates/nest/src/user/entities/user.entity.ts +66 -0
  68. package/templates/nest/src/user/user.controller.ts +172 -0
  69. package/templates/nest/src/user/user.module.ts +15 -0
  70. package/templates/nest/src/user/user.repository.ts +61 -0
  71. package/templates/nest/src/user/user.service.ts +138 -0
  72. package/templates/nest/template.json +5 -0
  73. package/templates/nest/test/jest-e2e.json +12 -0
  74. package/templates/nest/tsconfig.build.json +4 -0
  75. package/templates/nest/tsconfig.json +25 -0
  76. package/templates/python/.env.example +37 -0
  77. package/templates/python/Dockerfile +18 -0
  78. package/templates/python/README.md +219 -0
  79. package/templates/python/addons/.github/dependabot.yml +11 -0
  80. package/templates/python/addons/.github/labeler.yml +29 -0
  81. package/templates/python/addons/.github/workflows/check-branch.yml +24 -0
  82. package/templates/python/addons/.github/workflows/ci.yml +30 -0
  83. package/templates/python/addons/.github/workflows/draft-release.yml +22 -0
  84. package/templates/python/addons/.github/workflows/pr-labeler.yml +16 -0
  85. package/templates/python/addons/.github/workflows/release.yml +30 -0
  86. package/templates/python/addons/.github/workflows/security-audit.yml +21 -0
  87. package/templates/python/addons/.github/workflows/semantic-pr.yml +31 -0
  88. package/templates/python/addons/.github/workflows/stale.yml +24 -0
  89. package/templates/python/addons/addons.json +17 -0
  90. package/templates/python/addons/cloudinary/service.py +67 -0
  91. package/templates/python/addons/pre-commit/.pre-commit-config.yaml +31 -0
  92. package/templates/python/alembic/env.py +56 -0
  93. package/templates/python/alembic/script.py.mako +26 -0
  94. package/templates/python/alembic/versions/.gitkeep +0 -0
  95. package/templates/python/alembic.ini +39 -0
  96. package/templates/python/app/__init__.py +0 -0
  97. package/templates/python/app/auth/__init__.py +5 -0
  98. package/templates/python/app/auth/dependencies.py +118 -0
  99. package/templates/python/app/auth/enums.py +6 -0
  100. package/templates/python/app/auth/models.py +18 -0
  101. package/templates/python/app/auth/router.py +68 -0
  102. package/templates/python/app/auth/schemas.py +58 -0
  103. package/templates/python/app/auth/service.py +180 -0
  104. package/templates/python/app/common/__init__.py +18 -0
  105. package/templates/python/app/common/base_model.py +26 -0
  106. package/templates/python/app/common/base_repository.py +83 -0
  107. package/templates/python/app/common/errors.py +35 -0
  108. package/templates/python/app/common/pagination.py +22 -0
  109. package/templates/python/app/common/schemas.py +20 -0
  110. package/templates/python/app/common/utils.py +15 -0
  111. package/templates/python/app/core/__init__.py +4 -0
  112. package/templates/python/app/core/config.py +55 -0
  113. package/templates/python/app/core/database.py +20 -0
  114. package/templates/python/app/main.py +33 -0
  115. package/templates/python/app/user/__init__.py +4 -0
  116. package/templates/python/app/user/models.py +26 -0
  117. package/templates/python/app/user/repository.py +84 -0
  118. package/templates/python/app/user/router.py +170 -0
  119. package/templates/python/app/user/schemas.py +60 -0
  120. package/templates/python/app/user/service.py +114 -0
  121. package/templates/python/docker-compose.yml +55 -0
  122. package/templates/python/pyproject.toml +46 -0
  123. package/templates/python/requirements-dev.txt +7 -0
  124. package/templates/python/requirements.txt +20 -0
  125. package/templates/python/template.json +5 -0
  126. package/utils/template.js +165 -0
  127. package/utils/git.js +0 -71
@@ -0,0 +1,20 @@
1
+ from enum import StrEnum
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class SortOrder(StrEnum):
7
+ ASC = "ASC"
8
+ DESC = "DESC"
9
+
10
+
11
+ class BaseQueryParams(BaseModel):
12
+ """Parâmetros base de query com paginação e ordenação."""
13
+
14
+ page: int = Field(default=1, ge=1, description="Número da página que deseja buscar.")
15
+ limit: int = Field(
16
+ default=10, ge=1, le=100, description="Quantidade de itens por página."
17
+ )
18
+ search: str | None = Field(default=None, description="Termo de busca.")
19
+ sort_by: str = Field(default="created_at", description="Coluna de ordenação.")
20
+ sort_order: SortOrder = Field(default=SortOrder.DESC, description="Direção da ordenação.")
@@ -0,0 +1,15 @@
1
+ import re
2
+ import unicodedata
3
+
4
+
5
+ def create_slug(title: str) -> str:
6
+ """Gera um slug URL-friendly a partir de um título."""
7
+ slug = unicodedata.normalize("NFD", title.lower())
8
+ slug = re.sub(r"[\u0300-\u036f]", "", slug) # Remove acentos
9
+ slug = re.sub(r"[^a-z0-9]+", "-", slug)
10
+ slug = slug.strip("-")
11
+
12
+ if len(slug) > 50:
13
+ slug = slug[:50].rstrip("-")
14
+
15
+ return slug
@@ -0,0 +1,4 @@
1
+ from app.core.config import settings
2
+ from app.core.database import get_db
3
+
4
+ __all__ = ["settings", "get_db"]
@@ -0,0 +1,55 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
6
+
7
+ # Database
8
+ DATABASE_HOST: str = "localhost"
9
+ DATABASE_PORT: int = 5432
10
+ DATABASE_USERNAME: str = "postgres"
11
+ DATABASE_PASSWORD: str = "docker"
12
+ DATABASE_NAME: str = "innovationhub"
13
+ DATABASE_SSL: bool = False
14
+
15
+ # JWT
16
+ JWT_SECRET: str = "change-me"
17
+ JWT_EXPIRATION_MINUTES: int = 15
18
+ JWT_REFRESH_SECRET: str = "change-me-refresh"
19
+ JWT_REFRESH_EXPIRATION_DAYS: int = 7
20
+
21
+ # Default password
22
+ DEFAULT_PASSWORD: str = "ih123"
23
+
24
+ # CORS
25
+ CORS_ORIGIN: str = "http://localhost:3001"
26
+
27
+ # Mail
28
+ MAIL_HOST: str = "smtp.gmail.com"
29
+ MAIL_PORT: int = 587
30
+ MAIL_USER: str = ""
31
+ MAIL_PASSWORD: str = ""
32
+ MAIL_FROM: str = ""
33
+
34
+ @property
35
+ def database_url(self) -> str:
36
+ scheme = "postgresql+asyncpg"
37
+ return (
38
+ f"{scheme}://{self.DATABASE_USERNAME}:{self.DATABASE_PASSWORD}"
39
+ f"@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
40
+ )
41
+
42
+ @property
43
+ def database_url_sync(self) -> str:
44
+ """URL síncrona para uso com Alembic."""
45
+ return (
46
+ f"postgresql://{self.DATABASE_USERNAME}:{self.DATABASE_PASSWORD}"
47
+ f"@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
48
+ )
49
+
50
+ @property
51
+ def cors_origins(self) -> list[str]:
52
+ return [origin.strip() for origin in self.CORS_ORIGIN.split(",") if origin.strip()]
53
+
54
+
55
+ settings = Settings()
@@ -0,0 +1,20 @@
1
+ from collections.abc import AsyncGenerator
2
+
3
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
4
+
5
+ from app.core.config import settings
6
+
7
+ engine = create_async_engine(settings.database_url, echo=False, future=True)
8
+
9
+ async_session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
10
+
11
+
12
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
13
+ """Dependency que fornece uma sessão async do banco de dados."""
14
+ async with async_session_factory() as session:
15
+ try:
16
+ yield session
17
+ await session.commit()
18
+ except Exception:
19
+ await session.rollback()
20
+ raise
@@ -0,0 +1,33 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+
4
+ from app.auth.router import router as auth_router
5
+ from app.core.config import settings
6
+ from app.user.router import router as user_router
7
+
8
+ app = FastAPI(
9
+ title="API da Landing Page/Blog",
10
+ description="Documentação da API do backend (FastAPI) para Autenticação e Blog.",
11
+ version="1.0.0",
12
+ docs_url="/api/docs",
13
+ redoc_url="/api/redoc",
14
+ openapi_url="/api/openapi.json",
15
+ )
16
+
17
+ # CORS
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=settings.cors_origins,
21
+ allow_credentials=True,
22
+ allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
23
+ allow_headers=["Content-Type", "Authorization"],
24
+ )
25
+
26
+ # Routers
27
+ app.include_router(auth_router)
28
+ app.include_router(user_router)
29
+
30
+
31
+ @app.get("/", tags=["health"])
32
+ async def health_check():
33
+ return {"status": "ok"}
@@ -0,0 +1,4 @@
1
+ from app.user.models import User
2
+ from app.user.router import router
3
+
4
+ __all__ = ["User", "router"]
@@ -0,0 +1,26 @@
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import Boolean, Column, DateTime, Enum, String
4
+ from sqlalchemy.orm import relationship
5
+
6
+ from app.auth.enums import Role
7
+ from app.common.base_model import BaseModel
8
+
9
+
10
+ class User(BaseModel):
11
+
12
+ __tablename__ = "users"
13
+
14
+ email: str = Column(String, unique=True, nullable=False, index=True)
15
+ name: str = Column(String, nullable=False)
16
+ phone: str | None = Column(String, nullable=True)
17
+ password: str = Column(String, nullable=False)
18
+ is_active: bool = Column(Boolean, default=True, nullable=False)
19
+ role: str = Column(Enum(Role, name="role_enum"), default=Role.USER, nullable=False)
20
+ must_change_password: bool = Column(Boolean, default=False, nullable=False)
21
+ deleted_at: datetime | None = Column(DateTime(timezone=True), nullable=True)
22
+
23
+ # Relationships
24
+ refresh_tokens = relationship(
25
+ "RefreshToken", back_populates="user", cascade="all, delete-orphan"
26
+ )
@@ -0,0 +1,84 @@
1
+ import uuid
2
+
3
+ from sqlalchemy import desc, func, or_, select
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from app.common.base_repository import BaseRepository
7
+ from app.common.pagination import PaginatedResult, PaginationMeta
8
+ from app.user.models import User
9
+ from app.user.schemas import QueryUsersParams
10
+
11
+
12
+ class UserRepository(BaseRepository[User]):
13
+ """Repositório de usuários."""
14
+
15
+ def __init__(self, session: AsyncSession):
16
+ super().__init__(User, session)
17
+
18
+ async def find_by_id(self, id: uuid.UUID) -> User | None:
19
+ """Busca usuário ativo (sem soft delete) pelo ID."""
20
+ stmt = select(User).where(User.id == id, User.deleted_at.is_(None))
21
+ result = await self.session.execute(stmt)
22
+ return result.scalar_one_or_none()
23
+
24
+ async def find_all(self) -> list[User]:
25
+ """Busca todos os usuários ativos."""
26
+ stmt = select(User).where(User.deleted_at.is_(None)).order_by(desc(User.created_at))
27
+ result = await self.session.execute(stmt)
28
+ return list(result.scalars().all())
29
+
30
+ async def find_by_email(self, email: str) -> User | None:
31
+ """Busca usuário pelo email."""
32
+ stmt = select(User).where(User.email == email)
33
+ result = await self.session.execute(stmt)
34
+ return result.scalar_one_or_none()
35
+
36
+ async def find_and_count_users(self, query: QueryUsersParams) -> PaginatedResult[User]:
37
+ """Busca paginada com filtro por nome/email."""
38
+ base_stmt = select(User).where(User.deleted_at.is_(None))
39
+ count_stmt = select(func.count()).select_from(User).where(User.deleted_at.is_(None))
40
+
41
+ if query.search:
42
+ search_filter = or_(
43
+ User.name.ilike(f"%{query.search}%"),
44
+ User.email.ilike(f"%{query.search}%"),
45
+ )
46
+ base_stmt = base_stmt.where(search_filter)
47
+ count_stmt = count_stmt.where(search_filter)
48
+
49
+ # Whitelist de colunas para ordenação
50
+ allowed_columns = {"id", "name", "email", "is_active", "role", "created_at"}
51
+ sort_col = query.sort_by if query.sort_by in allowed_columns else "created_at"
52
+ column = getattr(User, sort_col, User.created_at)
53
+ order = desc(column) if query.sort_order.value == "DESC" else column.asc()
54
+
55
+ base_stmt = base_stmt.order_by(order)
56
+ base_stmt = base_stmt.offset((query.page - 1) * query.limit).limit(query.limit)
57
+
58
+ total_result = await self.session.execute(count_stmt)
59
+ total_items = total_result.scalar() or 0
60
+
61
+ result = await self.session.execute(base_stmt)
62
+ users = list(result.scalars().all())
63
+
64
+ total_pages = (total_items + query.limit - 1) // query.limit if query.limit > 0 else 0
65
+
66
+ return PaginatedResult(
67
+ data=users,
68
+ meta=PaginationMeta(
69
+ total_items=total_items,
70
+ item_count=len(users),
71
+ items_per_page=query.limit,
72
+ total_pages=total_pages,
73
+ current_page=query.page,
74
+ ),
75
+ )
76
+
77
+ async def soft_delete(self, id: uuid.UUID) -> None:
78
+ """Soft delete (marca deleted_at)."""
79
+ from datetime import UTC, datetime
80
+
81
+ user = await self.find_by_id(id)
82
+ if user:
83
+ user.deleted_at = datetime.now(UTC)
84
+ await self.session.flush()
@@ -0,0 +1,170 @@
1
+ import uuid
2
+
3
+ from fastapi import APIRouter, Depends, Query, status
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from app.auth.dependencies import get_current_user, require_role
7
+ from app.auth.enums import Role
8
+ from app.core.database import get_db
9
+ from app.user import service as user_service
10
+ from app.user.models import User
11
+ from app.user.schemas import (
12
+ CreateUserRequest,
13
+ QueryUsersParams,
14
+ UpdateUserRequest,
15
+ UserResponse,
16
+ )
17
+
18
+ router = APIRouter(prefix="/users", tags=["users"])
19
+
20
+
21
+ # --- Rotas do próprio usuário ---
22
+
23
+
24
+ @router.get(
25
+ "/me",
26
+ response_model=UserResponse,
27
+ summary="Retorna o perfil do usuário logado",
28
+ responses={200: {"description": "Perfil do usuário."}},
29
+ )
30
+ async def get_me(
31
+ current_user: User = Depends(get_current_user),
32
+ db: AsyncSession = Depends(get_db),
33
+ ):
34
+ return await user_service.get_user_by_id(db, current_user.id)
35
+
36
+
37
+ @router.patch(
38
+ "/me",
39
+ response_model=UserResponse,
40
+ summary="Atualiza o perfil do usuário logado",
41
+ responses={200: {"description": "Perfil atualizado com sucesso."}},
42
+ )
43
+ async def update_me(
44
+ data: UpdateUserRequest,
45
+ current_user: User = Depends(get_current_user),
46
+ db: AsyncSession = Depends(get_db),
47
+ ):
48
+ return await user_service.update_user_profile(db, current_user.id, data)
49
+
50
+
51
+ # --- Rotas administrativas ---
52
+
53
+
54
+ @router.post(
55
+ "/",
56
+ response_model=UserResponse,
57
+ status_code=status.HTTP_201_CREATED,
58
+ summary="Cria um novo usuário (admin)",
59
+ responses={201: {"description": "Usuário criado com sucesso."}},
60
+ )
61
+ async def create_user(
62
+ data: CreateUserRequest,
63
+ _admin: User = Depends(require_role(Role.ADMIN)),
64
+ db: AsyncSession = Depends(get_db),
65
+ ):
66
+ return await user_service.create_user(db, data)
67
+
68
+
69
+ @router.get(
70
+ "/",
71
+ response_model=list[UserResponse],
72
+ summary="Busca todos os usuários",
73
+ responses={200: {"description": "Lista de usuários retornada com sucesso."}},
74
+ )
75
+ async def find_all(
76
+ _admin: User = Depends(require_role(Role.ADMIN)),
77
+ db: AsyncSession = Depends(get_db),
78
+ ):
79
+ return await user_service.get_all_users(db)
80
+
81
+
82
+ @router.get(
83
+ "/paginated",
84
+ summary="Busca todos os usuários com paginação",
85
+ responses={200: {"description": "Lista de usuários retornada com sucesso."}},
86
+ )
87
+ async def find_paginated(
88
+ page: int = Query(1, ge=1),
89
+ limit: int = Query(10, ge=1, le=100),
90
+ search: str | None = Query(None),
91
+ sort_by: str = Query("id"),
92
+ sort_order: str = Query("ASC"),
93
+ _admin: User = Depends(require_role(Role.ADMIN)),
94
+ db: AsyncSession = Depends(get_db),
95
+ ):
96
+ from app.common.schemas import SortOrder as SortOrderEnum
97
+
98
+ query = QueryUsersParams(
99
+ page=page,
100
+ limit=limit,
101
+ search=search,
102
+ sort_by=sort_by,
103
+ sort_order=SortOrderEnum(sort_order.upper()),
104
+ )
105
+ return await user_service.get_users_paginated(db, query)
106
+
107
+
108
+ @router.get(
109
+ "/{user_id}",
110
+ response_model=UserResponse,
111
+ summary="Busca usuário pelo Id",
112
+ responses={
113
+ 200: {"description": "Usuário encontrado."},
114
+ 404: {"description": "Usuário não encontrado."},
115
+ },
116
+ )
117
+ async def find_by_id(
118
+ user_id: uuid.UUID,
119
+ _admin: User = Depends(require_role(Role.ADMIN)),
120
+ db: AsyncSession = Depends(get_db),
121
+ ):
122
+ return await user_service.get_user_by_id(db, user_id)
123
+
124
+
125
+ @router.patch(
126
+ "/{user_id}",
127
+ status_code=status.HTTP_204_NO_CONTENT,
128
+ summary="Atualiza um usuário pelo ID (admin)",
129
+ responses={204: {"description": "Usuário atualizado com sucesso."}},
130
+ )
131
+ async def update_user(
132
+ user_id: uuid.UUID,
133
+ data: UpdateUserRequest,
134
+ _admin: User = Depends(require_role(Role.ADMIN)),
135
+ db: AsyncSession = Depends(get_db),
136
+ ):
137
+ await user_service.update_user_profile(db, user_id, data)
138
+
139
+
140
+ @router.patch(
141
+ "/{user_id}/reset-password",
142
+ summary="Reseta a senha de um usuário (admin)",
143
+ responses={
144
+ 200: {"description": "Senha resetada com sucesso."},
145
+ 404: {"description": "Usuário não encontrado."},
146
+ },
147
+ )
148
+ async def reset_password(
149
+ user_id: uuid.UUID,
150
+ _admin: User = Depends(require_role(Role.ADMIN)),
151
+ db: AsyncSession = Depends(get_db),
152
+ ):
153
+ return await user_service.reset_password_by_admin(db, user_id)
154
+
155
+
156
+ @router.delete(
157
+ "/{user_id}",
158
+ status_code=status.HTTP_204_NO_CONTENT,
159
+ summary="Deleta um usuário (admin)",
160
+ responses={
161
+ 204: {"description": "Usuário deletado com sucesso."},
162
+ 404: {"description": "Usuário não encontrado."},
163
+ },
164
+ )
165
+ async def delete_user(
166
+ user_id: uuid.UUID,
167
+ _admin: User = Depends(require_role(Role.ADMIN)),
168
+ db: AsyncSession = Depends(get_db),
169
+ ):
170
+ await user_service.delete_user(db, user_id)
@@ -0,0 +1,60 @@
1
+ import uuid
2
+
3
+ from pydantic import BaseModel, EmailStr, Field
4
+
5
+ from app.auth.enums import Role
6
+ from app.common.schemas import BaseQueryParams
7
+
8
+ # --- Create ---
9
+
10
+
11
+ class CreateUserRequest(BaseModel):
12
+ """Schema para criação de usuário."""
13
+
14
+ email: EmailStr
15
+ name: str
16
+ is_active: bool | None = True
17
+ role: Role | None = Role.USER
18
+
19
+
20
+ # --- Update ---
21
+
22
+
23
+ class UpdateUserRequest(BaseModel):
24
+ """Schema para atualização de usuário."""
25
+
26
+ email: EmailStr | None = None
27
+ name: str | None = None
28
+ phone: str | None = None
29
+ is_active: bool | None = None
30
+ role: Role | None = None
31
+
32
+
33
+ # --- Response ---
34
+
35
+
36
+ class UserResponse(BaseModel):
37
+ """Schema de resposta do usuário (sem password)."""
38
+
39
+ model_config = {"from_attributes": True}
40
+
41
+ id: uuid.UUID
42
+ email: str
43
+ name: str
44
+ phone: str | None = None
45
+ is_active: bool
46
+ role: Role
47
+ must_change_password: bool
48
+
49
+
50
+ # --- Query ---
51
+
52
+
53
+ class QueryUsersParams(BaseQueryParams):
54
+ """Query params para busca de usuários."""
55
+
56
+ sort_by: str = Field(
57
+ default="id",
58
+ description="Coluna de ordenação.",
59
+ json_schema_extra={"enum": ["id", "name", "email", "is_active", "role"]},
60
+ )
@@ -0,0 +1,114 @@
1
+ import uuid
2
+
3
+ from fastapi import HTTPException, status
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from app.auth.service import hash_password
7
+ from app.common.errors import ERRORS
8
+ from app.common.pagination import PaginatedResult
9
+ from app.core.config import settings
10
+ from app.user.models import User
11
+ from app.user.repository import UserRepository
12
+ from app.user.schemas import CreateUserRequest, QueryUsersParams, UpdateUserRequest
13
+
14
+
15
+ async def create_user(db: AsyncSession, data: CreateUserRequest) -> User:
16
+ """Cria um novo usuário com senha padrão."""
17
+ repo = UserRepository(db)
18
+
19
+ existing = await repo.find_by_email(data.email)
20
+ if existing:
21
+ raise HTTPException(
22
+ status_code=status.HTTP_400_BAD_REQUEST,
23
+ detail=f"{ERRORS['USER']['EMAIL_IN_USE']} (Email: {data.email})",
24
+ )
25
+
26
+ default_password = settings.DEFAULT_PASSWORD
27
+ if not default_password:
28
+ raise HTTPException(
29
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
30
+ detail=ERRORS["USER"]["DEFAULT_PASSWORD_NOT_SET"],
31
+ )
32
+
33
+ hashed = hash_password(default_password)
34
+
35
+ return await repo.create({
36
+ **data.model_dump(exclude_unset=True),
37
+ "password": hashed,
38
+ "must_change_password": True,
39
+ })
40
+
41
+
42
+ async def get_user_by_id(db: AsyncSession, user_id: uuid.UUID) -> User:
43
+ """Busca um usuário pelo ID, levantando 404 se não encontrado."""
44
+ repo = UserRepository(db)
45
+ user = await repo.find_by_id(user_id)
46
+ if not user:
47
+ raise HTTPException(
48
+ status_code=status.HTTP_404_NOT_FOUND,
49
+ detail=ERRORS["USER"]["NOT_FOUND"],
50
+ )
51
+ return user
52
+
53
+
54
+ async def get_all_users(db: AsyncSession) -> list[User]:
55
+ """Lista todos os usuários ativos."""
56
+ repo = UserRepository(db)
57
+ return await repo.find_all()
58
+
59
+
60
+ async def get_users_paginated(db: AsyncSession, query: QueryUsersParams) -> PaginatedResult[User]:
61
+ """Lista usuários com paginação e busca."""
62
+ repo = UserRepository(db)
63
+ return await repo.find_and_count_users(query)
64
+
65
+
66
+ async def update_user_profile(
67
+ db: AsyncSession, user_id: uuid.UUID, data: UpdateUserRequest
68
+ ) -> User:
69
+ """Atualiza o perfil de um usuário."""
70
+ repo = UserRepository(db)
71
+ user = await repo.find_by_id(user_id)
72
+ if not user:
73
+ raise HTTPException(
74
+ status_code=status.HTTP_404_NOT_FOUND,
75
+ detail=ERRORS["USER"]["NOT_FOUND"],
76
+ )
77
+
78
+ update_data = data.model_dump(exclude_unset=True)
79
+ for key, value in update_data.items():
80
+ setattr(user, key, value)
81
+
82
+ await db.flush()
83
+ await db.refresh(user)
84
+ return user
85
+
86
+
87
+ async def reset_password_by_admin(db: AsyncSession, user_id: uuid.UUID) -> dict:
88
+ """Reseta a senha de um usuário para a senha padrão."""
89
+ user = await get_user_by_id(db, user_id)
90
+
91
+ default_password = settings.DEFAULT_PASSWORD
92
+ if not default_password:
93
+ raise HTTPException(
94
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
95
+ detail=ERRORS["USER"]["DEFAULT_PASSWORD_NOT_SET"],
96
+ )
97
+
98
+ user.password = hash_password(default_password)
99
+ user.must_change_password = True
100
+ await db.flush()
101
+
102
+ return {"message": f"A senha do usuário {user.name} foi resetada com sucesso"}
103
+
104
+
105
+ async def delete_user(db: AsyncSession, user_id: uuid.UUID) -> None:
106
+ """Soft delete de um usuário."""
107
+ repo = UserRepository(db)
108
+ user = await repo.find_by_id(user_id)
109
+ if not user:
110
+ raise HTTPException(
111
+ status_code=status.HTTP_404_NOT_FOUND,
112
+ detail=ERRORS["USER"]["NOT_FOUND"],
113
+ )
114
+ await repo.soft_delete(user_id)
@@ -0,0 +1,55 @@
1
+ version: "3.9"
2
+
3
+ services:
4
+ api-prod:
5
+ container_name: api_prod
6
+ build:
7
+ context: .
8
+ dockerfile: Dockerfile
9
+ target: production
10
+ ports:
11
+ - "3000:3000"
12
+ env_file:
13
+ - .env
14
+ depends_on:
15
+ postgres:
16
+ condition: service_healthy
17
+ restart: unless-stopped
18
+ profiles: ["prod"]
19
+
20
+ api-dev:
21
+ container_name: api_dev
22
+ build:
23
+ context: .
24
+ dockerfile: Dockerfile
25
+ target: development
26
+ ports:
27
+ - "3000:3000"
28
+ env_file:
29
+ - .env
30
+ depends_on:
31
+ postgres:
32
+ condition: service_healthy
33
+ volumes:
34
+ - .:/app
35
+ restart: unless-stopped
36
+ profiles: ["dev"]
37
+
38
+ postgres:
39
+ image: postgres:15
40
+ container_name: postgres
41
+ restart: unless-stopped
42
+ healthcheck:
43
+ test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
44
+ interval: 10s
45
+ timeout: 5s
46
+ retries: 5
47
+ env_file:
48
+ - .env
49
+ ports:
50
+ - "5432:5432"
51
+ volumes:
52
+ - db_data:/var/lib/postgresql/data
53
+
54
+ volumes:
55
+ db_data: