Skip to content

Commit 6ae3193

Browse files
author
User
committed
nits
1 parent 6f5c5d8 commit 6ae3193

File tree

15 files changed

+405
-68
lines changed

15 files changed

+405
-68
lines changed

backend/.env.example

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Database
2+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/copilot
3+
ASYNC_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/copilot
4+
5+
# Security
6+
SECRET_KEY=your-secret-key-here
7+
ALGORITHM=HS256
8+
ACCESS_TOKEN_EXPIRE_MINUTES=30
9+
REFRESH_TOKEN_EXPIRE_DAYS=7
10+
11+
# First superuser
12+
FIRST_SUPERUSER=[email protected]
13+
FIRST_SUPERUSER_PASSWORD=changeme
14+
15+
# Email (for password reset, etc.)
16+
SMTP_TLS=True
17+
SMTP_PORT=587
18+
SMTP_HOST=smtp.example.com
19+
20+
SMTP_PASSWORD=your-email-password
21+
EMAILS_FROM_EMAIL=[email protected]
22+
EMAILS_FROM_NAME="Copilot"
23+
24+
# OAuth (Google)
25+
GOOGLE_OAUTH_CLIENT_ID=your-google-client-id
26+
GOOGLE_OAUTH_CLIENT_SECRET=your-google-client-secret
27+
GOOGLE_OAUTH_REDIRECT_URI=http://localhost:8000/auth/google/callback
28+
29+
# OAuth (Microsoft)
30+
MICROSOFT_OAUTH_CLIENT_ID=your-microsoft-client-id
31+
MICROSOFT_OAUTH_CLIENT_SECRET=your-microsoft-client-secret
32+
MICROSOFT_OAUTH_REDIRECT_URI=http://localhost:8000/auth/microsoft/callback
33+
MICROSOFT_OAUTH_TENANT=common
34+
35+
# CORS (comma-separated list of origins, or * for all)
36+
BACKEND_CORS_ORIGINS=["http://localhost:3000", "http://localhost:8000"]
37+
38+
# Logging
39+
LOG_LEVEL=INFO
40+
SQL_ECHO=False
41+
42+
# Environment (development, staging, production)
43+
ENVIRONMENT=development

backend/app/api/deps.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@
88
from pydantic import ValidationError
99
from sqlmodel import Session
1010

11-
from app.core import security
12-
from app.core.config import settings
13-
from app.core.db import engine
11+
from app.core import security, SessionLocal, settings
12+
from app.core.config import settings as app_settings
1413
from app.models import TokenPayload, User
1514

1615
reusable_oauth2 = OAuth2PasswordBearer(
17-
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
16+
tokenUrl=f"{app_settings.API_V1_STR}/login/access-token"
1817
)
1918

2019

2120
def get_db() -> Generator[Session, None, None]:
22-
with Session(engine) as session:
23-
yield session
21+
db = SessionLocal()
22+
try:
23+
yield db
24+
finally:
25+
db.close()
2426

2527

2628
SessionDep = Annotated[Session, Depends(get_db)]

backend/app/api/routes/login.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from app.core import security
1111
from app.core.config import settings
1212
from app.core.security import get_password_hash
13-
from app.models import Message, NewPassword, Token, UserPublic
13+
from app.models import Message, NewPassword, TokenPair as Token, UserPublic
1414
from app.utils import (
1515
generate_password_reset_token,
1616
generate_reset_password_email,

backend/app/core/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
get_async_session,
1818
init_db,
1919
async_init_db,
20+
sync_engine,
21+
async_engine,
22+
SessionLocal,
23+
AsyncSessionLocal,
2024
)
2125

2226
# Security

backend/app/core/config.py

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def parse_cors(v: Any) -> list[str] | str:
3939

4040
class DatabaseSettings(BaseSettings):
4141
"""Database configuration settings."""
42+
model_config = SettingsConfigDict(env_prefix="DATABASE_")
4243

4344
POSTGRES_SERVER: str = "localhost"
4445
POSTGRES_USER: str = "postgres"
@@ -78,6 +79,7 @@ def ASYNC_SQLALCHEMY_DATABASE_URI(self) -> str:
7879

7980
class AuthSettings(BaseSettings):
8081
"""Authentication and authorization settings."""
82+
model_config = SettingsConfigDict(env_prefix="AUTH_")
8183

8284
SECRET_KEY: str = secrets.token_urlsafe(32)
8385
ALGORITHM: str = "HS256"
@@ -108,9 +110,9 @@ class AuthSettings(BaseSettings):
108110
SESSION_COOKIE_DOMAIN: Optional[str] = None
109111

110112
# CORS
111-
BACKEND_CORS_ORIGINS: list[AnyUrl] = [
112-
"http://localhost:3000",
113-
"http://localhost:8000",
113+
BACKEND_CORS_ORIGINS: list[HttpUrl] = [
114+
HttpUrl("http://localhost:3000"),
115+
HttpUrl("http://localhost:8000"),
114116
]
115117

116118
@property
@@ -121,6 +123,7 @@ def all_cors_origins(self) -> list[str]:
121123

122124
class EmailSettings(BaseSettings):
123125
"""Email configuration settings."""
126+
model_config = SettingsConfigDict(env_prefix="EMAIL_")
124127

125128
SMTP_TLS: bool = True
126129
SMTP_PORT: int = 587
@@ -138,6 +141,7 @@ def EMAILS_ENABLED(self) -> bool:
138141

139142
class RedisSettings(BaseSettings):
140143
"""Redis configuration settings."""
144+
model_config = SettingsConfigDict(env_prefix="REDIS_")
141145

142146
REDIS_HOST: str = "localhost"
143147
REDIS_PORT: int = 6379
@@ -159,10 +163,6 @@ def REDIS_URL(self) -> RedisDsn:
159163
)
160164

161165

162-
class Settings(DatabaseSettings, AuthSettings, EmailSettings, RedisSettings):
163-
"""Application settings."""
164-
165-
166166
def parse_cors(v: Any) -> list[str] | str:
167167
if isinstance(v, str) and not v.startswith("["):
168168
return [i.strip() for i in v.split(",")]
@@ -171,7 +171,8 @@ def parse_cors(v: Any) -> list[str] | str:
171171
raise ValueError(v)
172172

173173

174-
class Settings(BaseSettings):
174+
class Settings(DatabaseSettings, AuthSettings, EmailSettings, RedisSettings):
175+
"""Application settings."""
175176
model_config = SettingsConfigDict(
176177
env_file=PROJECT_ROOT / ".env",
177178
env_file_encoding="utf-8",
@@ -184,6 +185,15 @@ class Settings(BaseSettings):
184185
PROJECT_NAME: str = "Copilot API"
185186
API_V1_STR: str = "/api/v1"
186187
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
188+
189+
# Allow development as an alias for local
190+
@model_validator(mode='before')
191+
@classmethod
192+
def validate_environment(cls, data: Any) -> Any:
193+
if isinstance(data, dict) and data.get('ENVIRONMENT') == 'development':
194+
data['ENVIRONMENT'] = 'local'
195+
return data
196+
187197
DEBUG: bool = False
188198

189199
# Security
@@ -255,27 +265,36 @@ def set_debug(cls, v: str) -> str:
255265
return v
256266

257267
@field_validator("BACKEND_CORS_ORIGINS", mode="before")
258-
def assemble_cors_origins(cls, v: Union[str, list[str]]) -> list[str] | str:
268+
def assemble_cors_origins(cls, v: Union[str, list[Union[str, HttpUrl]]]) -> list[HttpUrl]:
259269
"""Parse CORS origins from a comma-separated string or list."""
260-
if isinstance(v, str) and not v.startswith("["):
261-
return [i.strip() for i in v.split(",")]
262-
elif isinstance(v, list):
263-
return v
264-
raise ValueError(v)
265-
266-
class Config:
267-
case_sensitive = True
268-
env_file = ".env"
269-
env_file_encoding = "utf-8"
270-
270+
if isinstance(v, str):
271+
if v.startswith("["):
272+
# Handle JSON array string
273+
import json
274+
v = json.loads(v)
275+
else:
276+
# Handle comma-separated string
277+
v = [i.strip() for i in v.split(",")]
278+
279+
# Convert all items to HttpUrl objects
280+
result = []
281+
for item in v:
282+
if isinstance(item, str):
283+
result.append(HttpUrl(item))
284+
elif isinstance(item, HttpUrl):
285+
result.append(item)
286+
else:
287+
raise ValueError(f"Invalid CORS origin: {item}")
288+
return result
289+
271290
PROJECT_NAME: str
272291
SENTRY_DSN: HttpUrl | None = None
273-
POSTGRES_SERVER: str
292+
POSTGRES_SERVER: str = "localhost"
293+
POSTGRES_USER: str = "postgres"
294+
POSTGRES_PASSWORD: str = "postgres"
295+
POSTGRES_DB: str = "copilot"
274296
POSTGRES_PORT: int = 5432
275-
POSTGRES_USER: str
276-
POSTGRES_PASSWORD: str = ""
277-
POSTGRES_DB: str = ""
278-
297+
279298
@computed_field # type: ignore[prop-decorator]
280299
@property
281300
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
@@ -295,7 +314,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
295314
SMTP_USER: str | None = None
296315
SMTP_PASSWORD: str | None = None
297316
EMAILS_FROM_EMAIL: EmailStr | None = None
298-
EMAILS_FROM_NAME: EmailStr | None = None
317+
EMAILS_FROM_NAME: str | None = None
299318

300319
@model_validator(mode="after")
301320
def _set_default_emails_from(self) -> Self:

backend/app/core/logging.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from typing import Any, Dict, Optional
99

1010
from loguru import logger
11-
from pydantic import BaseSettings, Field
11+
from pydantic import Field
12+
from pydantic_settings import BaseSettings
1213

1314

1415
class LoggingSettings(BaseSettings):

backend/app/core/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast
2121

2222
import jwt
23-
from fastapi import HTTPException, status
23+
from fastapi import HTTPException, Request, status
2424
from jose import jwe
2525
from passlib.context import CryptContext
2626
from pydantic import BaseModel, EmailStr, ValidationError

backend/app/crud.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def update_user(*, session: Session, db_user: User, user_in: UserUpdate) -> Any:
3333

3434
def get_user_by_email(*, session: Session, email: str) -> User | None:
3535
statement = select(User).where(User.email == email)
36-
session_user = session.exec(statement).first()
36+
# Use execute() instead of exec() for SQLAlchemy compatibility
37+
session_user = session.execute(statement).scalars().first()
3738
return session_user
3839

3940

backend/app/db/__init__.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,24 @@
1-
# This file makes the db directory a Python package
1+
"""
2+
Database package.
3+
4+
This package provides database session management and utilities.
5+
"""
6+
from app.core.db import (
7+
get_db,
8+
get_async_db,
9+
get_sync_session,
10+
get_async_session,
11+
init_db,
12+
async_init_db,
13+
get_password_hash,
14+
)
15+
16+
__all__ = [
17+
'get_db',
18+
'get_async_db',
19+
'get_sync_session',
20+
'get_async_session',
21+
'init_db',
22+
'async_init_db',
23+
'get_password_hash',
24+
]

backend/app/models/__init__.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Import all models here so they're properly registered with SQLAlchemy
2+
from sqlmodel import SQLModel
3+
24
from app.models.base import BaseDBModel, TimestampMixin
35
from app.models.user import (
46
UserRole,
57
OAuthProvider,
68
UserBase,
79
UserCreate,
810
UserUpdate,
9-
UserInDB,
11+
UserInDB as User,
1012
UserPublic,
1113
UserLogin,
1214
TokenPayload,
@@ -17,6 +19,21 @@
1719
RefreshTokenPublic,
1820
PasswordResetRequest,
1921
PasswordResetConfirm,
22+
NewPassword,
23+
UpdatePassword,
24+
UserRegister,
25+
UserUpdateMe,
26+
UsersPublic,
27+
)
28+
29+
from app.models.item import (
30+
Item,
31+
ItemBase,
32+
ItemCreate,
33+
ItemUpdate,
34+
ItemPublic,
35+
ItemsPublic,
36+
Message,
2037
)
2138

2239
# This ensures that SQLModel knows about all models for migrations
@@ -28,7 +45,7 @@
2845
'UserBase',
2946
'UserCreate',
3047
'UserUpdate',
31-
'UserInDB',
48+
'User',
3249
'UserPublic',
3350
'UserLogin',
3451
'TokenPayload',
@@ -39,4 +56,16 @@
3956
'RefreshTokenPublic',
4057
'PasswordResetRequest',
4158
'PasswordResetConfirm',
59+
'NewPassword',
60+
'UpdatePassword',
61+
'UserRegister',
62+
'UserUpdateMe',
63+
'UsersPublic',
64+
'Item',
65+
'ItemBase',
66+
'ItemCreate',
67+
'ItemUpdate',
68+
'ItemPublic',
69+
'ItemsPublic',
70+
'Message',
4271
]

0 commit comments

Comments
 (0)