diff --git a/.env.example b/.env.example index cc29ffd..a4ec410 100755 --- a/.env.example +++ b/.env.example @@ -3,7 +3,15 @@ # API Configuration PYTHONPATH=/server -# Add any additional environment variables your application might need -# DATABASE_URL=postgresql://user:password@localhost:5432/pynews -# DEBUG=false -# LOG_LEVEL=info +# SQLite Database Configuration +SQLITE_PATH=/app/data/pynewsdb.db +SQLITE_URL=sqlite+aiosqlite:////app/data/pynewsdb.db + +# Authentication Configuration +SECRET_KEY=1... +ENCRYPTION_KEY=r0... +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=20 +ADMIN_USER=admin +ADMIN_PASSWORD=admin +ADMIN_EMAIL=ADMIN_USER@mail.com diff --git a/.gitignore b/.gitignore index 5bed03b..3fb254e 100755 --- a/.gitignore +++ b/.gitignore @@ -208,3 +208,4 @@ __marimo__/ # SQLiteDB pynewsdb.db +data/ diff --git a/Dockerfile b/Dockerfile index dc69c8e..05502f0 100755 --- a/Dockerfile +++ b/Dockerfile @@ -59,5 +59,6 @@ RUN poetry install --no-root --no-interaction WORKDIR $PROJECT_PATH COPY app app COPY tests tests +COPY scripts scripts CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--lifespan", "on"] diff --git a/Makefile b/Makefile index fb6da07..b02b1d5 100755 --- a/Makefile +++ b/Makefile @@ -64,6 +64,45 @@ health: ## Verifica o health check da API @echo "$(YELLOW)Verificando saúde da API...$(NC)" curl -f http://localhost:8000/api/healthcheck || echo "API não está respondendo" +db-backup: ## Cria backup do banco SQLite + @echo "$(YELLOW)Criando backup do banco...$(NC)" + @if [ -f "./data/pynewsdb.db" ]; then \ + cp ./data/pynewsdb.db ./data/pynewsdb.db.backup-$(shell date +%Y%m%d_%H%M%S); \ + echo "$(GREEN)Backup criado com sucesso!$(NC)"; \ + else \ + echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \ + fi + +db-restore: ## Restaura backup do banco SQLite (usar: make db-restore BACKUP=filename) + @echo "$(YELLOW)Restaurando backup do banco...$(NC)" + @if [ -z "$(BACKUP)" ]; then \ + echo "Use: make db-restore BACKUP=filename"; \ + exit 1; \ + fi + @if [ -f "./data/$(BACKUP)" ]; then \ + cp ./data/$(BACKUP) ./data/pynewsdb.db; \ + echo "$(GREEN)Backup restaurado com sucesso!$(NC)"; \ + else \ + echo "Arquivo de backup não encontrado: ./data/$(BACKUP)"; \ + fi + +db-reset: ## Remove o banco SQLite (será recriado na próxima execução) + @echo "$(YELLOW)Removendo banco de dados...$(NC)" + @if [ -f "./data/pynewsdb.db" ]; then \ + rm ./data/pynewsdb.db; \ + echo "$(GREEN)Banco removido. Será recriado na próxima execução.$(NC)"; \ + else \ + echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \ + fi + +db-shell: ## Abre shell SQLite para interagir com o banco + @echo "$(YELLOW)Abrindo shell SQLite...$(NC)" + @if [ -f "./data/pynewsdb.db" ]; then \ + sqlite3 ./data/pynewsdb.db; \ + else \ + echo "Banco de dados não encontrado em ./data/pynewsdb.db"; \ + fi + install: ## Instala dependências com Poetry @echo "$(YELLOW)Instalando dependências...$(NC)" poetry install @@ -78,3 +117,21 @@ setup: install build up ## Setup completo do projeto docker/test: docker exec -e PYTHONPATH=/app $(API_CONTAINER_NAME) pytest -s --cov-report=term-missing --cov-report html --cov-report=xml --cov=app tests/ + +create-community: ## Cria uma nova comunidade (usar: make create-community NAME=nome EMAIL=email PASSWORD=senha) + @echo "$(YELLOW)Criando nova comunidade...$(NC)" + @if [ -z "$(NAME)" ] || [ -z "$(EMAIL)" ] || [ -z "$(PASSWORD)" ]; then \ + echo "Use: make create-community NAME=nome EMAIL=email PASSWORD=senha"; \ + exit 1; \ + fi + docker exec $(API_CONTAINER_NAME) python scripts/create_community.py "$(NAME)" "$(EMAIL)" "$(PASSWORD)" + @echo "$(GREEN)Comunidade criada com sucesso!$(NC)" + +exec-script: ## Executa um script dentro do container (usar: make exec-script SCRIPT=caminho/script.py ARGS="arg1 arg2") + @echo "$(YELLOW)Executando script no container...$(NC)" + @if [ -z "$(SCRIPT)" ]; then \ + echo "Use: make exec-script SCRIPT=caminho/script.py ARGS=\"arg1 arg2\""; \ + exit 1; \ + fi + docker exec $(API_CONTAINER_NAME) python $(SCRIPT) $(ARGS) + @echo "$(GREEN)Script executado!$(NC)" diff --git a/README.md b/README.md index a2886ba..182619c 100755 --- a/README.md +++ b/README.md @@ -111,6 +111,16 @@ sequenceDiagram - Documentação Swagger: http://localhost:8000/docs - Health Check: http://localhost:8000/api/healthcheck +### 🗄️ Banco de Dados SQLite + +O projeto utiliza SQLite como banco de dados com as seguintes características: +- **Persistência**: Dados armazenados em `./data/pynewsdb.db` +- **Async Support**: Utiliza `aiosqlite` para operações assíncronas +- **ORM**: SQLModel para mapeamento objeto-relacional +- **Auto-inicialização**: Banco e tabelas criados automaticamente na primeira execução + +Para mais detalhes sobre configuração do SQLite, consulte: [docs/sqlite-setup.md](docs/sqlite-setup.md) + ## 🧩 Configuração Inicial ### ▶️ Guia de Execução para Desenvolvimento diff --git a/app/routers/healthcheck/routes.py b/app/routers/healthcheck/routes.py index 2e45e7c..04715ae 100755 --- a/app/routers/healthcheck/routes.py +++ b/app/routers/healthcheck/routes.py @@ -1,12 +1,14 @@ from fastapi import APIRouter, Request, status from pydantic import BaseModel +from app.services.database.database import get_session from app.services.limiter import limiter class HealthCheckResponse(BaseModel): status: str = "healthy" version: str = "2.0.0" + database: str = "connected" def setup(): @@ -23,7 +25,18 @@ def setup(): async def healthcheck(request: Request): """ Health check endpoint that returns the current status of the API. + Includes database connectivity check. """ - return HealthCheckResponse() + database_status = "connected" + + try: + # Test database connection by getting a session + async for _ in get_session(): + # If we can get a session, the database is connected + break + except Exception: + database_status = "disconnected" + + return HealthCheckResponse(database=database_status) return router diff --git a/app/services/database/database.py b/app/services/database/database.py index 7bed98c..6322771 100644 --- a/app/services/database/database.py +++ b/app/services/database/database.py @@ -2,11 +2,19 @@ import os from typing import AsyncGenerator +from sqlalchemy import text from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlmodel import Field, SQLModel from sqlmodel.ext.asyncio.session import AsyncSession from app.services.database import models # noqa F401 +from app.services.database.models import ( # noqa F401 + Community, + Library, + LibraryRequest, + News, + Subscription, +) logger = logging.getLogger(__name__) @@ -32,7 +40,7 @@ engine, class_=AsyncSession, expire_on_commit=False, - #echo=True, # expire_on_commit=False é importante! + # echo=True, # expire_on_commit=False é importante! ) @@ -59,24 +67,57 @@ async def init_db(): """ Inicializa o banco de dados: 1. Verifica se o arquivo do banco de dados existe. - 2. Se não existir, cria o arquivo e todas as tabelas definidas - nos modelos SQLModel nos imports e acima. + 2. Conecta ao banco e verifica se as tabelas existem. + 3. Cria tabelas faltantes se necessário. """ - if not os.path.exists(DATABASE_FILE): - logger.info( - f"Arquivo de banco de dados '{DATABASE_FILE}' não encontrado." - f"Criando novo banco de dados e tabelas." - ) + try: + # Cria o diretório do banco se não existir + db_dir = os.path.dirname(DATABASE_FILE) + if db_dir and not os.path.exists(db_dir): + os.makedirs(db_dir, exist_ok=True) + logger.info(f"Diretório criado: {db_dir}") + + # Verifica se o arquivo existe + db_exists = os.path.exists(DATABASE_FILE) + + if not db_exists: + logger.info( + f"Arquivo de banco de dados '{DATABASE_FILE}' não encontrado. " + f"Criando novo banco de dados." + ) + else: + logger.info(f"Conectando ao banco de dados '{DATABASE_FILE}'.") + + # Sempre tenta criar as tabelas (create_all é idempotente) + # Se as tabelas já existem, o SQLModel não fará nada async with engine.begin() as conn: # SQLModel.metadata.create_all é síncrono e precisa # ser executado via run_sync await conn.run_sync(SQLModel.metadata.create_all) - logger.info("Tabelas criadas com sucesso.") - else: - logger.info( - f"Arquivo de banco de dados '{DATABASE_FILE}'" - f"já existe. Conectando." - ) + + # Verifica quais tabelas foram criadas/existem + async with AsyncSessionLocal() as session: + result = await session.execute( + text( + "SELECT name FROM sqlite_master WHERE type='table' " + "ORDER BY name" + ) + ) + tables = [row[0] for row in result.fetchall()] + + if not db_exists: + message = "Banco de dados e tabelas criados com sucesso." + logger.info(message) + else: + message = "Estrutura do banco de dados verificada." + logger.info(message) + + tables_message = f"Tabelas disponíveis: {', '.join(tables)}" + logger.info(tables_message) + + except Exception as e: + logger.error(f"Erro ao inicializar banco de dados: {e}") + raise async def get_session() -> AsyncGenerator[AsyncSession, None]: diff --git a/app/services/database/models/communities.py b/app/services/database/models/communities.py index bb66894..13bdb21 100644 --- a/app/services/database/models/communities.py +++ b/app/services/database/models/communities.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Optional -from sqlalchemy import Text +from sqlalchemy import Column, String from sqlmodel import Field, SQLModel @@ -10,7 +10,9 @@ class Community(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) username: str - email: str = Field(sa_column=Text) # VARCHAR(255) + email: str = Field( + sa_column=Column("email", String, unique=True) + ) # VARCHAR(255), unique key password: str created_at: Optional[datetime] = Field(default_factory=datetime.now) updated_at: Optional[datetime] = Field( diff --git a/docker-compose.yaml b/docker-compose.yaml index f54f765..48cc04d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '3.8' - services: pynews-api: build: @@ -9,18 +7,16 @@ services: container_name: pynews-server ports: - "8000:8000" - environment: - - PYTHONPATH=/server - - SQLITE_PATH=app/services/database/pynewsdb.db - - SQLITE_URL=sqlite+aiosqlite:// - - SECRET_KEY=1a6c5f3b7d2e4a7fb68d0casd3f9a7b2d8c4e5f6a3b0d4e9c7a8f1b6d3c0a7f5e - - ENCRYPTION_KEY=r0-QKv5qACJNFRqy2cNZCsfZ_zVvehlC-v8zDJb--EI= - - ADMIN_USER=admin - - ADMIN_PASSWORD=admin - - ADMIN_EMAIL=ADMIN_USER@mail.com - - ALGORITHM=HS256 - - ACCESS_TOKEN_EXPIRE_MINUTES=20 + env_file: + - .env restart: unless-stopped + volumes: + - sqlite_data:/app/data + environment: + - SQLITE_PATH=/app/data/pynewsdb.db + - SQLITE_URL=sqlite+aiosqlite:////app/data/pynewsdb.db + depends_on: + - sqlite-init healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/healthcheck"] interval: 30s @@ -28,6 +24,30 @@ services: retries: 3 start_period: 40s + sqlite-init: + image: alpine:latest + container_name: pynews-sqlite-init + volumes: + - sqlite_data:/data + command: > + sh -c " + mkdir -p /data && + touch /data/pynewsdb.db && + chmod 777 /data && + chmod 666 /data/pynewsdb.db && + chown -R root:root /data && + echo 'SQLite database initialized' + " + restart: "no" + +volumes: + sqlite_data: + driver: local + driver_opts: + type: none + o: bind + device: ./data + networks: default: name: pynews-network diff --git a/docs/sqlite-setup.md b/docs/sqlite-setup.md new file mode 100644 index 0000000..0cc6df9 --- /dev/null +++ b/docs/sqlite-setup.md @@ -0,0 +1,141 @@ +# SQLite Service Setup + +This document describes the SQLite service configuration for the PyNewsServer project. + +## Overview + +The SQLite service is configured as part of the Docker Compose setup to provide persistent database storage for the application. + +## Architecture + +- **sqlite-init**: An initialization service that creates the SQLite database file and sets proper permissions +- **pynews-api**: The main application service that connects to the SQLite database +- **sqlite_data volume**: A bind mount volume that maps to `./data` directory for data persistence + +## Configuration + +### Environment Variables + +The following environment variables are used for SQLite configuration: + +- `SQLITE_PATH`: Path to the SQLite database file (default: `/app/data/pynewsdb.db`) +- `SQLITE_URL`: SQLAlchemy connection URL (default: `sqlite+aiosqlite:///app/data/pynewsdb.db`) + +### Volume Mapping + +- **Host Path**: `./data` +- **Container Path**: `/app/data` +- **Database File**: `pynewsdb.db` + +## Services + +### sqlite-init + +This service: +- Creates the data directory if it doesn't exist +- Creates an empty SQLite database file +- Sets proper file permissions (664) +- Sets proper ownership (1000:1000) +- Runs only once and exits + +### pynews-api + +The main application service: +- Depends on `sqlite-init` to ensure database initialization +- Mounts the SQLite data volume +- Uses async SQLite operations via `aiosqlite` +- Automatically creates tables on first run + +## Usage + +### Starting the Services + +```bash +docker-compose up -d +``` + +This will: +1. Start the `sqlite-init` service to initialize the database +2. Start the `pynews-api` service after initialization is complete + +### Accessing the Database + +The SQLite database file is located at: +- **Host**: `./data/pynewsdb.db` +- **Container**: `/app/data/pynewsdb.db` + +### Database Operations + +The application uses SQLModel with async SQLAlchemy for database operations: + +- **Connection**: Async SQLite with `aiosqlite` +- **ORM**: SQLModel (built on SQLAlchemy) +- **Sessions**: Async session management +- **Migrations**: Automatic table creation via SQLModel metadata + +## Data Persistence + +Database data is persisted in the `./data` directory on the host system. This directory is: +- Created automatically by the services +- Excluded from Git via `.gitignore` +- Bound to the container's `/app/data` directory + +## Backup and Recovery + +### Backup + +```bash +# Copy database file +cp ./data/pynewsdb.db ./data/pynewsdb.db.backup + +# Or use SQLite dump +sqlite3 ./data/pynewsdb.db .dump > backup.sql +``` + +### Recovery + +```bash +# Restore from backup file +cp ./data/pynewsdb.db.backup ./data/pynewsdb.db + +# Or restore from SQL dump +sqlite3 ./data/pynewsdb.db < backup.sql +``` + +## Troubleshooting + +### Permission Issues + +If you encounter permission issues: + +```bash +# Fix ownership +sudo chown 1000:1000 ./data/pynewsdb.db + +# Fix permissions +chmod 664 ./data/pynewsdb.db +``` + +### Database Corruption + +If the database becomes corrupted: + +```bash +# Remove corrupted database +rm ./data/pynewsdb.db + +# Restart services to recreate +docker-compose restart +``` + +### Volume Issues + +If volume mounting fails: + +```bash +# Ensure data directory exists +mkdir -p ./data + +# Check Docker permissions +ls -la ./data/ +``` diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..995ba88 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,108 @@ +# Community Creation Scripts + +This directory contains Python scripts to create new communities in the PyNewsServer database with properly hashed passwords. + +## Available Scripts + +### 1. `create_community.py` - Command Line Script + +**Usage:** +```bash +python scripts/create_community.py +``` + +**Example:** +```bash +python scripts/create_community.py john_doe john@example.com mypassword123 +``` + +**Features:** +- Takes command line arguments for username, email, and password +- Automatically hashes the password using the `hash_password` function from `app.services.auth` +- Creates the community record in the database +- Returns success confirmation with community details + +### 2. `add_community.py` - Interactive Script + +**Usage:** +```bash +python scripts/add_community.py +``` + +**Features:** +- Interactive prompt for username, email, and password input +- Confirmation step before creating the community +- Password is hidden in the confirmation display +- Uses the same `hash_password` function for security + +### 3. `example_create_community.py` - Example/Demo Script + +**Usage:** +```bash +python scripts/example_create_community.py +``` + +**Features:** +- Demonstrates how to create a community programmatically +- Shows the password hashing process step by step +- Good for understanding the implementation +- Creates a sample community with hardcoded values + +## Important Notes + +### Security +- All scripts use the `hash_password` function from `app.services.auth` +- Passwords are hashed using bcrypt with salt before storage +- Plain text passwords are never stored in the database + +### Database +- Scripts automatically initialize the database if it doesn't exist +- Uses the same async database session management as the main application +- All database operations are handled asynchronously + +### Requirements +- Python virtual environment must be activated +- All dependencies from `requirements.txt` must be installed +- Scripts must be run from the project root directory + +## Running with Virtual Environment + +If you have a virtual environment set up (recommended): + +```bash +# Activate the virtual environment first +source .venv/bin/activate # On Linux/Mac +# or +.venv\Scripts\activate # On Windows + +# Then run any script +python scripts/create_community.py username email password +``` + +Or use the full path to the Python executable: + +```bash +/path/to/your/project/.venv/bin/python scripts/create_community.py username email password +``` + +## Example Output + +``` +Creating community for user: john_doe +✅ Community created successfully! + ID: 1 + Username: john_doe + Email: john@example.com + Created at: 2025-10-14 12:02:15.778722 +``` + +## Integration with Main Application + +These scripts use the same: +- Database models (`app.services.database.models.communities.Community`) +- Authentication functions (`app.services.auth.hash_password`) +- Database session management (`app.services.database.database`) + +This ensures consistency with the main application's authentication and data handling. + +```` diff --git a/scripts/add_community.py b/scripts/add_community.py new file mode 100644 index 0000000..b25fca4 --- /dev/null +++ b/scripts/add_community.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +Script to add a new community to the Community table with hashed password. + +Usage: + python scripts/add_community.py + +The script will prompt for community details and hash the password. +""" + +import asyncio +import os +import sys + +# Add the app directory to the Python path so we can import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from app.services.auth import hash_password +from app.services.database.database import AsyncSessionLocal, init_db +from app.services.database.models.communities import Community + + +async def add_community(username: str, email: str, password: str) -> Community: + """ + Add a new community to the database with hashed password. + + Args: + username (str): The community username + email (str): The community email + password (str): The plain text password (will be hashed) + + Returns: + Community: The created community object + """ + # Hash the password before storing it + hashed_pwd = hash_password(password) + + # Create new community instance + new_community = Community( + username=username, + email=email, + password=hashed_pwd.decode("utf-8"), # Convert bytes to string + ) + + # Save to database + async with AsyncSessionLocal() as session: + session.add(new_community) + await session.commit() + await session.refresh(new_community) + + return new_community + + +def get_user_input(prompt: str) -> str: + """Get user input synchronously.""" + return input(prompt).strip() + + +async def main(): + """Main function to run the script interactively.""" + try: + # Initialize database if needed + await init_db() + + print("=== Add New Community ===") + print("Please provide the following information:") + + # Get user input + username = get_user_input("Username: ") + if not username: + print("Error: Username cannot be empty") + return + + email = get_user_input("Email: ") + if not email: + print("Error: Email cannot be empty") + return + + password = get_user_input("Password: ") + if not password: + print("Error: Password cannot be empty") + return + + # Confirm before creating + print("\nCreating community with:") + print(f" Username: {username}") + print(f" Email: {email}") + print(" Password: [HIDDEN]") + + confirm = get_user_input("\nProceed? (y/N): ").lower() + if confirm not in {"y", "yes"}: + print("Operation cancelled.") + return + + # Create the community + print("\nCreating community...") + community = await add_community(username, email, password) + + print("✅ Community created successfully!") + print(f" ID: {community.id}") + print(f" Username: {community.username}") + print(f" Email: {community.email}") + print(f" Created at: {community.created_at}") + + except KeyboardInterrupt: + print("\nOperation cancelled by user.") + except Exception as e: + print(f"❌ Error creating community: {e}") + raise + + +def create_community_programmatically( + username: str, email: str, password: str +) -> Community: + """ + Wrapper function to create a community programmatically. + For use in other scripts. + + Args: + username (str): The community username + email (str): The community email + password (str): The plain text password + + Returns: + Community: The created community object + """ + return asyncio.run(add_community(username, email, password)) + + +if __name__ == "__main__": + # Run the interactive script + asyncio.run(main()) diff --git a/scripts/create_community.py b/scripts/create_community.py new file mode 100644 index 0000000..f017978 --- /dev/null +++ b/scripts/create_community.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Simple script to add a community with command line arguments. + +Usage: + python scripts/create_community.py + +Example: + python scripts/create_community.py john_doe john@example.com mypassword123 +""" + +import asyncio +import os +import sys + +# Add the app directory to the Python path so we can import modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from app.services.auth import hash_password # noqa: E402 +from app.services.database.database import ( # noqa: E402 + AsyncSessionLocal, + init_db, +) +from app.services.database.models.communities import Community # noqa: E402 + + +async def create_community( + username: str, email: str, password: str +) -> Community: + """ + Create a new community in the database with hashed password. + + Args: + username (str): The community username + email (str): The community email + password (str): The plain text password (will be hashed) + + Returns: + Community: The created community object + """ + # Initialize database if needed + await init_db() + + # Hash the password before storing it + hashed_pwd = hash_password(password) + + # Create new community instance + new_community = Community( + username=username, + email=email, + password=hashed_pwd.decode("utf-8"), # Convert bytes to string + ) + + # Save to database + async with AsyncSessionLocal() as session: + session.add(new_community) + await session.commit() + await session.refresh(new_community) + + return new_community + + +async def main(): + """Main function to create community from command line arguments.""" + if len(sys.argv) != 4: + print( + "Usage: python scripts/create_community.py " + " " + ) + print( + "Example: python scripts/create_community.py " + "john_doe john@example.com mypass123" + ) + sys.exit(1) + + username = sys.argv[1] + email = sys.argv[2] + password = sys.argv[3] + + try: + print(f"Creating community for user: {username}") + community = await create_community(username, email, password) + + print("✅ Community created successfully!") + print(f" ID: {community.id}") + print(f" Username: {community.username}") + print(f" Email: {community.email}") + print(f" Created at: {community.created_at}") + + except Exception as e: + print(f"❌ Error creating community: {e}") + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/example_create_community.py b/scripts/example_create_community.py new file mode 100644 index 0000000..0240d9e --- /dev/null +++ b/scripts/example_create_community.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Example script demonstrating how to create a community programmatically. + +This script shows how to use the create_community function to add +a new community with a hashed password to the database. +""" + +import asyncio +import os +import sys + +# Add the app directory to the Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from app.services.auth import hash_password # noqa: E402 +from app.services.database.database import ( # noqa: E402 + AsyncSessionLocal, + init_db, +) +from app.services.database.models.communities import Community # noqa: E402 + + +async def create_community_example(): + """Example of creating a community with hashed password.""" + + # Initialize database + await init_db() + print("Database initialized successfully") + + # Community data + username = "example_user" + email = "example@domain.com" + plain_password = "my_secure_password_123" + + print(f"Creating community for: {username}") + print(f"Email: {email}") + print("Password will be hashed before storing") + + # Hash the password using the hash_password function + hashed_password = hash_password(plain_password) + print(f"Password hashed: {hashed_password[:20]}... (truncated)") + + # Create new community instance + new_community = Community( + username=username, + email=email, + password=hashed_password.decode("utf-8"), # Convert bytes to string + ) + + # Save to database using async session + async with AsyncSessionLocal() as session: + session.add(new_community) + await session.commit() + await session.refresh(new_community) + + print("\n✅ Community created successfully!") + print(f" ID: {new_community.id}") + print(f" Username: {new_community.username}") + print(f" Email: {new_community.email}") + print(f" Created at: {new_community.created_at}") + print(f" Updated at: {new_community.updated_at}") + + # Verify the password hash is stored correctly + print(f" Stored password hash: {new_community.password[:30]}...") + + +if __name__ == "__main__": + asyncio.run(create_community_example())