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:
parent
112ebdbf7d
commit
ef4fa26931
22
README.md
22
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
|
||||
|
||||
|
@ -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)
|
@ -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.
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user