Skip to content

Commit a343345

Browse files
committed
feat(deploy): implement automated initial superuser creation via environment variables
1 parent e23f25b commit a343345

5 files changed

Lines changed: 71 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ O deploy da aplicação é realizado na plataforma **Render** utilizando **Docke
8888
- **Infraestrutura:** Definida no arquivo `render.yaml` (Blueprint).
8989
- **Banco de Dados:** PostgreSQL (gerenciado pela Render).
9090
- **Processo:** O `Dockerfile` realiza o build otimizado com `uv`, executa as migrações do Alembic e inicia o servidor Uvicorn.
91+
- **Inicialização:** No primeiro deploy, se as variáveis de ambiente `FIRST_SUPERUSER_USERNAME` e `FIRST_SUPERUSER_PASSWORD` estiverem configuradas, o sistema criará automaticamente o primeiro usuário gerente (caso ele ainda não exista).
9192
- **Configuração:** O validador em `src/core/config.py` converte automaticamente a URL do banco para o driver assíncrono `postgresql+asyncpg://`.
9293

9394
---

render.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ services:
1313
generateValue: true
1414
- key: ENVIRONMENT
1515
value: production
16+
- key: FIRST_SUPERUSER_USERNAME
17+
sync: false
18+
- key: FIRST_SUPERUSER_PASSWORD
19+
sync: false
1620

1721
databases:
1822
- name: fastapibank-db

scripts/entrypoint.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#!/bin/sh
22
alembic upgrade head
3+
python -m src.commands.init_db
34
exec uvicorn src.main:app --host 0.0.0.0 --port 8000

src/commands/init_db.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Database initialization script.
2+
3+
Checks if an initial superuser should be created and creates it if needed.
4+
"""
5+
6+
import asyncio
7+
import logging
8+
9+
from sqlalchemy.ext.asyncio import create_async_engine
10+
from sqlmodel import select
11+
from sqlmodel.ext.asyncio.session import AsyncSession
12+
13+
from src.core.config import settings
14+
from src.models.user import User
15+
from src.schemas.user import UserIn
16+
from src.utils.password import get_password_hash
17+
18+
logging.basicConfig(level=logging.INFO)
19+
logger = logging.getLogger(__name__)
20+
21+
22+
async def init_db() -> None:
23+
"""Create the initial superuser if configured."""
24+
if (
25+
not settings.first_superuser_username
26+
or not settings.first_superuser_password
27+
):
28+
logger.info(
29+
"First superuser credentials not configured. Skipping initialization."
30+
)
31+
return
32+
33+
engine = create_async_engine(settings.database_url)
34+
35+
async with AsyncSession(engine) as session:
36+
statement = select(User).where(
37+
User.username == settings.first_superuser_username
38+
)
39+
result = await session.exec(statement)
40+
user = result.first()
41+
42+
if user:
43+
logger.info("Superuser already exists. Skipping creation.")
44+
else:
45+
logger.info("Creating initial superuser.")
46+
user_in = UserIn(
47+
username=settings.first_superuser_username,
48+
plain_password=settings.first_superuser_password,
49+
)
50+
new_user = User(**user_in.model_dump(exclude={"plain_password"}))
51+
new_user.hashed_password = get_password_hash(
52+
user_in.plain_password
53+
)
54+
session.add(new_user)
55+
await session.commit()
56+
logger.info("Initial superuser created successfully.")
57+
58+
await engine.dispose()
59+
60+
61+
if __name__ == "__main__":
62+
asyncio.run(init_db())

src/core/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ def assemble_db_url(cls, v: str) -> str:
4545
algorithm: str = Field(default="HS256")
4646
access_token_expire_minutes: int = Field(default=30)
4747

48+
first_superuser_username: str | None = Field(default=None)
49+
first_superuser_password: str | None = Field(default=None)
50+
4851
model_config = SettingsConfigDict(env_file=".env")
4952

5053

0 commit comments

Comments
 (0)