Remove authentication requirements for open access API

- Replace authentication system with automatic default user creation
- Update all API endpoints to work without authentication
- Modify user endpoints to work with default user
- Update README.md to reflect the open access model
- Fix linting issues and ensure code quality
This commit is contained in:
Automated Action 2025-05-27 19:23:34 +00:00
parent 112ebdbf7d
commit ef4fa26931
4 changed files with 67 additions and 129 deletions

View File

@ -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

View File

@ -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
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)

View File

@ -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.

View File

@ -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: