diff --git a/README.md b/README.md index f5c1405..4e362d5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A FastAPI application that aggregates news from various sources using the Medias ## Features -- User authentication and authorization +- Open API access without authentication - News article fetching and storage from Mediastack API - Filtering news by keywords, sources, categories, countries, and languages - User preferences for personalized news @@ -18,7 +18,6 @@ A FastAPI application that aggregates news from various sources using the Medias - **SQLAlchemy**: ORM for database interactions - **Alembic**: Database migration tool - **Pydantic**: Data validation and settings management -- **JWT**: Token-based authentication ## Requirements @@ -36,11 +35,6 @@ A FastAPI application that aggregates news from various sources using the Medias ``` # Required for fetching news from Mediastack API MEDIASTACK_API_KEY=your_api_key_here - - # Optional - For JWT authentication security - # If not set, a secure random key will be generated on startup - # but JWT tokens will be invalidated when the server restarts - SECRET_KEY=your_secret_key_here ``` 4. Run database migrations: @@ -59,11 +53,11 @@ The API is documented with Swagger UI, available at `/docs` when the server is r ### Main Endpoints -- `/api/v1/users/register`: Register a new user -- `/api/v1/users/token`: Login and get JWT token +- `/api/v1/users/default`: Get the default user for open API access - `/api/v1/news`: Get news articles with optional filtering - `/api/v1/news/personalized`: Get personalized news based on user preferences -- `/api/v1/news/saved`: Save articles for later reading +- `/api/v1/news/refresh`: Fetch fresh news from Mediastack API +- `/api/v1/news/saved`: Save or retrieve articles for later reading - `/health`: Health check endpoint ## Project Structure @@ -76,13 +70,13 @@ The API is documented with Swagger UI, available at `/docs` when the server is r - `app/services`: Business logic services - `migrations`: Database migrations -## Security Notes +## Access Model -The application uses two distinct keys for different purposes: +This application uses an open access model without authentication: -1. **Mediastack API Key**: Used to authenticate with the external Mediastack news API service. This key is required to fetch news data. +1. **Mediastack API Key**: The only key required is for the external Mediastack news API service. This key is used to fetch news data and must be provided in the MEDIASTACK_API_KEY environment variable. -2. **Secret Key**: Used internally for JWT token generation and verification in the authentication system. If not provided via the SECRET_KEY environment variable, a secure random key will be generated automatically on each startup. Note that this means all active JWT tokens will be invalidated when the server restarts. +2. **Default User**: The application automatically creates and uses a default user for all operations that would normally require authentication. This makes it easy to use the API without needing to create accounts or manage tokens. ## Development diff --git a/app/api/deps.py b/app/api/deps.py index 9a290e5..025ae7c 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -1,16 +1,9 @@ from typing import Generator - -from fastapi import Depends, HTTPException, status -from jose import jwt, JWTError -from pydantic import ValidationError +from fastapi import Depends from sqlalchemy.orm import Session -from app.core.config import settings -from app.core.security import ALGORITHM, oauth2_scheme from app.db.session import SessionLocal from app.models.user import User -from app.schemas.token import TokenPayload -from app.services.user import get_user def get_db() -> Generator: @@ -24,59 +17,56 @@ def get_db() -> Generator: db.close() -def get_current_user( - db: Session = Depends(get_db), token: str = Depends(oauth2_scheme) -) -> User: +def get_default_user(db: Session = Depends(get_db)) -> User: """ - Dependency to get the current authenticated user. + Get a default user for API access without authentication. + Returns the first superuser in the database, or creates one if none exists. """ - try: - payload = jwt.decode( - token, settings.SECRET_KEY, algorithms=[ALGORITHM] - ) - token_data = TokenPayload(**payload) - except (JWTError, ValidationError): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) + # Try to get the first user + user = db.query(User).filter(User.is_superuser).first() - user = get_user(db, user_id=token_data.sub) + # If no user exists, create a default superuser if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="User not found", + # Import here to avoid circular imports + from app.core.security import get_password_hash + from app.models.news import UserPreference + + # Create a default superuser + user = User( + email="admin@example.com", + username="admin", + hashed_password=get_password_hash("admin"), + is_active=True, + is_superuser=True, ) + db.add(user) + db.commit() + db.refresh(user) + + # Create default user preferences + user_preference = UserPreference(user_id=user.id) + db.add(user_preference) + db.commit() return user -def get_current_active_user( - current_user: User = Depends(get_current_user), -) -> User: +def get_current_user(db: Session = Depends(get_db)) -> User: """ - Dependency to get the current active user. + Dependency to get the current user without authentication. """ - if not current_user.is_active: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Inactive user", - ) - - return current_user + return get_default_user(db) -def get_current_active_superuser( - current_user: User = Depends(get_current_active_user), -) -> User: +def get_current_active_user(db: Session = Depends(get_db)) -> User: """ - Dependency to get the current active superuser. + Dependency to get the current active user without authentication. """ - if not current_user.is_superuser: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="The user doesn't have enough privileges", - ) - - return current_user \ No newline at end of file + return get_default_user(db) + + +def get_current_active_superuser(db: Session = Depends(get_db)) -> User: + """ + Dependency to get the current active superuser without authentication. + """ + return get_default_user(db) \ No newline at end of file diff --git a/app/api/v1/news.py b/app/api/v1/news.py index 278eddd..e74c411 100644 --- a/app/api/v1/news.py +++ b/app/api/v1/news.py @@ -34,7 +34,6 @@ async def get_news( categories: Optional[str] = None, countries: Optional[str] = None, languages: Optional[str] = None, - current_user: Optional[User] = Depends(deps.get_current_active_user), refresh: bool = False, background_tasks: BackgroundTasks = None, ): @@ -97,9 +96,9 @@ async def get_personalized_news( db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 25, - current_user: User = Depends(deps.get_current_active_user), refresh: bool = False, background_tasks: BackgroundTasks = None, + current_user: User = Depends(deps.get_current_user), ): """ Retrieve news articles based on the user's preferences. @@ -169,7 +168,6 @@ async def refresh_news( countries: Optional[str] = None, languages: Optional[str] = None, limit: int = 100, - current_user: User = Depends(deps.get_current_active_user), ): """ Fetch fresh news from Mediastack API and return them. @@ -216,7 +214,7 @@ async def get_saved_articles( db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100, - current_user: User = Depends(deps.get_current_active_user), + current_user: User = Depends(deps.get_current_user), ): """ Get articles saved by the current user. @@ -232,7 +230,7 @@ async def save_article( *, db: Session = Depends(deps.get_db), article_in: SavedArticleCreate, - current_user: User = Depends(deps.get_current_active_user), + current_user: User = Depends(deps.get_current_user), ): """ Save an article for the current user. @@ -271,7 +269,7 @@ async def save_article( async def remove_saved_article( saved_article_id: int, db: Session = Depends(deps.get_db), - current_user: User = Depends(deps.get_current_active_user), + current_user: User = Depends(deps.get_current_user), ): """ Remove a saved article for the current user. diff --git a/app/api/v1/users.py b/app/api/v1/users.py index 7558e76..fe37cff 100644 --- a/app/api/v1/users.py +++ b/app/api/v1/users.py @@ -1,80 +1,39 @@ from typing import Any, List -from datetime import timedelta from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session from app.api import deps -from app.core.config import settings -from app.core.security import create_access_token from app.models.user import User -from app.schemas.user import User as UserSchema, UserCreate, UserUpdate +from app.schemas.user import User as UserSchema, UserUpdate from app.schemas.news import UserPreference as UserPreferenceSchema, UserPreferenceUpdate -from app.schemas.token import Token from app.services.user import ( - authenticate_user, - create_user, - update_user, get_user_by_email, get_user, get_user_preference, update_user_preference, + update_user, ) router = APIRouter() -@router.post("/token", response_model=Token) -async def login_for_access_token( +@router.get("/default", response_model=UserSchema) +async def get_default_user( db: Session = Depends(deps.get_db), - form_data: OAuth2PasswordRequestForm = Depends(), ) -> Any: """ - OAuth2 compatible token login, get an access token for future requests. + Get the default user for open API access. """ - user = authenticate_user(db, form_data.username, form_data.password) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - - access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - return { - "access_token": create_access_token( - user.id, expires_delta=access_token_expires - ), - "token_type": "bearer", - } - - -@router.post("/register", response_model=UserSchema) -async def register_user( - *, - db: Session = Depends(deps.get_db), - user_in: UserCreate, -) -> Any: - """ - Register a new user. - """ - try: - user = create_user(db, user_in) - return user - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=str(e), - ) + return deps.get_default_user(db) @router.get("/me", response_model=UserSchema) async def read_users_me( - current_user: User = Depends(deps.get_current_active_user), + current_user: User = Depends(deps.get_current_user), ) -> Any: """ - Get current user. + Get current user (default user without authentication). """ return current_user @@ -84,10 +43,10 @@ async def update_user_me( *, db: Session = Depends(deps.get_db), user_in: UserUpdate, - current_user: User = Depends(deps.get_current_active_user), + current_user: User = Depends(deps.get_current_user), ) -> Any: """ - Update current user. + Update current user (default user without authentication). """ if user_in.email and user_in.email != current_user.email: if get_user_by_email(db, user_in.email): @@ -103,10 +62,10 @@ async def update_user_me( @router.get("/me/preferences", response_model=UserPreferenceSchema) async def read_user_preferences( db: Session = Depends(deps.get_db), - current_user: User = Depends(deps.get_current_active_user), + current_user: User = Depends(deps.get_current_user), ) -> Any: """ - Get current user's preferences. + Get current user's preferences (default user without authentication). """ preferences = get_user_preference(db, current_user.id) if not preferences: @@ -122,10 +81,10 @@ async def update_user_preferences( *, db: Session = Depends(deps.get_db), preferences_in: UserPreferenceUpdate, - current_user: User = Depends(deps.get_current_active_user), + current_user: User = Depends(deps.get_current_user), ) -> Any: """ - Update current user's preferences. + Update current user's preferences (default user without authentication). """ preferences = update_user_preference( db, @@ -139,16 +98,14 @@ async def update_user_preferences( return preferences -# Admin endpoints @router.get("/", response_model=List[UserSchema]) async def read_users( db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100, - current_user: User = Depends(deps.get_current_active_superuser), ) -> Any: """ - Retrieve users. Only for superusers. + Retrieve all users. """ users = db.query(User).offset(skip).limit(limit).all() return users @@ -158,10 +115,9 @@ async def read_users( async def read_user( user_id: int, db: Session = Depends(deps.get_db), - current_user: User = Depends(deps.get_current_active_superuser), ) -> Any: """ - Get a specific user by id. Only for superusers. + Get a specific user by id. """ user = get_user(db, user_id=user_id) if not user: