Skip to content

Commit 24ab6da

Browse files
authored
Merge pull request #181 from lbedner/auth-docs
Update Auth Docs
2 parents e35bc24 + 7c942c7 commit 24ab6da

19 files changed

Lines changed: 1177 additions & 670 deletions

File tree

aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ def main():
292292
remove_file("app/models/user.py")
293293
remove_dir("app/services/auth")
294294
remove_file("app/core/security.py")
295+
# Remove auth CLI
296+
remove_file("app/cli/auth.py")
295297
# Remove auth-related tests if they exist
296298
remove_file("tests/api/test_auth_endpoints.py")
297299
remove_file("tests/services/test_auth_service.py")

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/auth.py.j2

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Authentication CLI commands.
44
Command-line interface for auth service management tasks.
55
"""
66

7+
import asyncio
78
import secrets
89
import string
910
from typing import TYPE_CHECKING
@@ -13,7 +14,7 @@ if TYPE_CHECKING:
1314

1415
import typer
1516

16-
from app.core.db import db_session
17+
from app.core.db import get_async_session
1718
from app.models.user import UserCreate
1819
from app.services.auth.user_service import UserService
1920

@@ -26,12 +27,14 @@ def generate_password(length: int = 12) -> str:
2627
return "".join(secrets.choice(alphabet) for _ in range(length))
2728

2829

29-
def find_next_available_email(
30+
async def find_next_available_email(
3031
user_service: UserService, prefix: str = "test", domain: str = "example.com"
3132
) -> str:
3233
"""Find the next available email with auto-increment."""
3334
# Get existing emails with this prefix
34-
existing_emails = user_service.find_existing_emails_with_prefix(prefix, domain)
35+
existing_emails = await user_service.find_existing_emails_with_prefix(
36+
prefix, domain
37+
)
3538

3639
if not existing_emails:
3740
# No existing emails, start with prefix@domain
@@ -82,7 +85,17 @@ def create_test_user(
8285
),
8386
) -> None:
8487
"""Create a test user for development and testing."""
88+
asyncio.run(_create_test_user(email, password, full_name, prefix, domain))
8589

90+
91+
async def _create_test_user(
92+
email: str | None,
93+
password: str | None,
94+
full_name: str | None,
95+
prefix: str,
96+
domain: str,
97+
) -> None:
98+
"""Async implementation of create_test_user."""
8699
# Generate password if not provided
87100
if password is None:
88101
password = generate_password()
@@ -91,16 +104,16 @@ def create_test_user(
91104
generated_password = False
92105

93106
try:
94-
with db_session() as session:
107+
async with get_async_session() as session:
95108
user_service = UserService(session)
96109

97110
# Auto-generate email if not provided
98111
if email is None:
99-
email = find_next_available_email(user_service, prefix, domain)
112+
email = await find_next_available_email(user_service, prefix, domain)
100113
typer.echo(f"📧 Auto-generated email: {email} (next in sequence)")
101114

102115
# Check if user already exists
103-
existing_user = user_service.get_user_by_email(email)
116+
existing_user = await user_service.get_user_by_email(email)
104117
if existing_user:
105118
typer.echo(f"❌ User with email '{email}' already exists", err=True)
106119
raise typer.Exit(1)
@@ -113,7 +126,7 @@ def create_test_user(
113126
)
114127

115128
# Create the user
116-
user = user_service.create_user(user_data)
129+
user = await user_service.create_user(user_data)
117130

118131
# Display success message
119132
typer.echo("✅ Test user created successfully!")
@@ -145,7 +158,13 @@ def create_test_users(
145158
),
146159
) -> None:
147160
"""Create multiple test users for development and testing."""
161+
asyncio.run(_create_test_users(count, prefix, domain, password))
162+
148163

164+
async def _create_test_users(
165+
count: int, prefix: str, domain: str, password: str | None
166+
) -> None:
167+
"""Async implementation of create_test_users."""
149168
# Generate password if not provided
150169
if password is None:
151170
password = generate_password()
@@ -158,7 +177,7 @@ def create_test_users(
158177
raise typer.Exit(1)
159178

160179
try:
161-
with db_session() as session:
180+
async with get_async_session() as session:
162181
user_service = UserService(session)
163182
created_users = []
164183

@@ -167,7 +186,7 @@ def create_test_users(
167186

168187
for i in range(count):
169188
# Find next available email
170-
email = find_next_available_email(user_service, prefix, domain)
189+
email = await find_next_available_email(user_service, prefix, domain)
171190

172191
# Create user data
173192
user_data = UserCreate(
@@ -177,7 +196,7 @@ def create_test_users(
177196
)
178197

179198
# Create the user
180-
user = user_service.create_user(user_data)
199+
user = await user_service.create_user(user_data)
181200
created_users.append(user)
182201

183202
typer.echo(f"✅ Created: {user.email} (ID: {user.id})")
@@ -200,10 +219,15 @@ def create_test_users(
200219
@app.command()
201220
def list_users() -> None:
202221
"""List all users in the system."""
222+
asyncio.run(_list_users())
223+
224+
225+
async def _list_users() -> None:
226+
"""Async implementation of list_users."""
203227
try:
204-
with db_session() as session:
228+
async with get_async_session() as session:
205229
user_service = UserService(session)
206-
users = user_service.list_users()
230+
users = await user_service.list_users()
207231

208232
if not users:
209233
typer.echo("No users found.")

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/{% if cookiecutter.include_auth == "yes" %}auth.py{% endif %}

Lines changed: 0 additions & 2 deletions
This file was deleted.

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/router.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from app.core.security import create_access_token, verify_password
55
from app.models.user import UserCreate, UserResponse
66
from app.services.auth.auth_service import get_current_user_from_token
7-
from app.services.auth.user_service import AsyncUserService
7+
from app.services.auth.user_service import UserService
88
from fastapi import APIRouter, Depends, HTTPException, status
99
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
1010
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -17,7 +17,7 @@
1717
@router.post("/register", response_model=UserResponse)
1818
async def register(user_data: UserCreate, db: AsyncSession = Depends(get_async_db)):
1919
"""Register a new user."""
20-
user_service = AsyncUserService(db)
20+
user_service = UserService(db)
2121

2222
# Check if user already exists
2323
existing_user = await user_service.get_user_by_email(user_data.email)
@@ -37,7 +37,7 @@ async def login(
3737
db: AsyncSession = Depends(get_async_db),
3838
):
3939
"""Login and get access token."""
40-
user_service = AsyncUserService(db)
40+
user_service = UserService(db)
4141

4242
# Get user by email (username field in OAuth2 form)
4343
user = await user_service.get_user_by_email(form_data.username)

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/auth_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from app.core.security import verify_token
77
from app.models.user import User
8-
from app.services.auth.user_service import AsyncUserService
8+
from app.services.auth.user_service import UserService
99

1010

1111
async def get_current_user_from_token(token: str, db: AsyncSession) -> User:
@@ -27,7 +27,7 @@ async def get_current_user_from_token(token: str, db: AsyncSession) -> User:
2727
raise credentials_exception
2828

2929
# Get user from database
30-
user_service = AsyncUserService(db)
30+
user_service = UserService(db)
3131
user = await user_service.get_user_by_email(email)
3232
if user is None:
3333
raise credentials_exception

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/user_service.py

Lines changed: 2 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,87 +2,15 @@
22

33
from datetime import UTC, datetime
44

5-
from sqlmodel import Session, select
5+
from sqlmodel import select
66
from sqlmodel.ext.asyncio.session import AsyncSession
77

88
from app.core.security import get_password_hash
99
from app.models.user import User, UserCreate
1010

1111

1212
class UserService:
13-
"""Service for managing users."""
14-
15-
def __init__(self, db: Session):
16-
self.db = db
17-
18-
def create_user(self, user_data: UserCreate) -> User:
19-
"""Create a new user."""
20-
# Hash the password
21-
hashed_password = get_password_hash(user_data.password)
22-
23-
# Create user object
24-
user = User(
25-
email=user_data.email,
26-
full_name=user_data.full_name,
27-
hashed_password=hashed_password,
28-
is_active=user_data.is_active,
29-
created_at=datetime.now(UTC),
30-
)
31-
32-
# Save to database
33-
self.db.add(user)
34-
self.db.commit()
35-
self.db.refresh(user)
36-
37-
return user
38-
39-
def get_user_by_email(self, email: str) -> User | None:
40-
"""Get user by email address."""
41-
statement = select(User).where(User.email == email)
42-
result = self.db.exec(statement)
43-
return result.first()
44-
45-
def get_user_by_id(self, user_id: int) -> User | None:
46-
"""Get user by ID."""
47-
return self.db.get(User, user_id)
48-
49-
def update_user(self, user_id: int, **updates) -> User | None:
50-
"""Update user data."""
51-
user = self.get_user_by_id(user_id)
52-
if not user:
53-
return None
54-
55-
for field, value in updates.items():
56-
if hasattr(user, field):
57-
setattr(user, field, value)
58-
59-
user.updated_at = datetime.now(UTC)
60-
self.db.add(user)
61-
self.db.commit()
62-
self.db.refresh(user)
63-
64-
return user
65-
66-
def deactivate_user(self, user_id: int) -> User | None:
67-
"""Deactivate a user account."""
68-
return self.update_user(user_id, is_active=False)
69-
70-
def list_users(self) -> list[User]:
71-
"""List all users in the system."""
72-
statement = select(User).order_by(User.created_at.desc())
73-
result = self.db.exec(statement)
74-
return list(result.all())
75-
76-
def find_existing_emails_with_prefix(self, prefix: str, domain: str) -> list[str]:
77-
"""Find existing emails that match the pattern prefix{number}@domain."""
78-
pattern = f"{prefix}%@{domain}"
79-
statement = select(User.email).where(User.email.like(pattern))
80-
result = self.db.exec(statement)
81-
return list(result.all())
82-
83-
84-
class AsyncUserService:
85-
"""Async service for managing users with non-blocking database operations."""
13+
"""Service for managing users with async database operations."""
8614

8715
def __init__(self, db: AsyncSession):
8816
self.db = db

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_auth_endpoints.py.j2

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ login, token validation, and protected endpoint access.
88
import pytest
99
from app.core.security import create_access_token
1010
from app.models.user import UserCreate
11-
from app.services.auth.user_service import AsyncUserService, UserService
11+
from app.services.auth.user_service import UserService
1212
from fastapi import status
1313
from fastapi.testclient import TestClient
1414
from sqlmodel import Session
@@ -48,7 +48,7 @@ class TestAuthEndpoints:
4848
):
4949
"""Test registration with already existing email."""
5050
# Create a user first
51-
user_service = AsyncUserService(async_db_session)
51+
user_service = UserService(async_db_session)
5252
existing_user = UserCreate(
5353
email="existing@example.com",
5454
full_name="Existing User",
@@ -90,7 +90,7 @@ class TestAuthEndpoints:
9090
):
9191
"""Test login with valid credentials."""
9292
# Create a user first
93-
user_service = AsyncUserService(async_db_session)
93+
user_service = UserService(async_db_session)
9494
user_data = UserCreate(
9595
email="login@example.com",
9696
full_name="Login User",
@@ -131,7 +131,7 @@ class TestAuthEndpoints:
131131
):
132132
"""Test login with wrong password."""
133133
# Create a user first
134-
user_service = AsyncUserService(async_db_session)
134+
user_service = UserService(async_db_session)
135135
user_data = UserCreate(
136136
email="wrongpass@example.com",
137137
full_name="Wrong Pass User",
@@ -156,7 +156,7 @@ class TestAuthEndpoints:
156156
):
157157
"""Test getting current user with valid token."""
158158
# Create a user first
159-
user_service = AsyncUserService(async_db_session)
159+
user_service = UserService(async_db_session)
160160
user_data = UserCreate(
161161
email="currentuser@example.com",
162162
full_name="Current User",
@@ -204,7 +204,7 @@ class TestAuthEndpoints:
204204
):
205205
"""Test getting current user with expired token."""
206206
# Create a user first
207-
user_service = AsyncUserService(async_db_session)
207+
user_service = UserService(async_db_session)
208208
user_data = UserCreate(
209209
email="expiredtoken@example.com",
210210
full_name="Expired Token User",
@@ -298,7 +298,7 @@ class TestAuthIntegration:
298298
assert login_response.status_code == status.HTTP_200_OK
299299

300300
# Verify user exists in database
301-
user_service = AsyncUserService(async_db_session)
301+
user_service = UserService(async_db_session)
302302
db_user = await user_service.get_user_by_email(user_data["email"])
303303

304304
assert db_user is not None

0 commit comments

Comments
 (0)