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
|
## Features
|
||||||
|
|
||||||
- User authentication and authorization
|
- Open API access without authentication
|
||||||
- News article fetching and storage from Mediastack API
|
- News article fetching and storage from Mediastack API
|
||||||
- Filtering news by keywords, sources, categories, countries, and languages
|
- Filtering news by keywords, sources, categories, countries, and languages
|
||||||
- User preferences for personalized news
|
- 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
|
- **SQLAlchemy**: ORM for database interactions
|
||||||
- **Alembic**: Database migration tool
|
- **Alembic**: Database migration tool
|
||||||
- **Pydantic**: Data validation and settings management
|
- **Pydantic**: Data validation and settings management
|
||||||
- **JWT**: Token-based authentication
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@ -36,11 +35,6 @@ A FastAPI application that aggregates news from various sources using the Medias
|
|||||||
```
|
```
|
||||||
# Required for fetching news from Mediastack API
|
# Required for fetching news from Mediastack API
|
||||||
MEDIASTACK_API_KEY=your_api_key_here
|
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:
|
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
|
### Main Endpoints
|
||||||
|
|
||||||
- `/api/v1/users/register`: Register a new user
|
- `/api/v1/users/default`: Get the default user for open API access
|
||||||
- `/api/v1/users/token`: Login and get JWT token
|
|
||||||
- `/api/v1/news`: Get news articles with optional filtering
|
- `/api/v1/news`: Get news articles with optional filtering
|
||||||
- `/api/v1/news/personalized`: Get personalized news based on user preferences
|
- `/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
|
- `/health`: Health check endpoint
|
||||||
|
|
||||||
## Project Structure
|
## 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
|
- `app/services`: Business logic services
|
||||||
- `migrations`: Database migrations
|
- `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
|
## Development
|
||||||
|
|
||||||
|
@ -1,16 +1,9 @@
|
|||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
from fastapi import Depends
|
||||||
from fastapi import Depends, HTTPException, status
|
|
||||||
from jose import jwt, JWTError
|
|
||||||
from pydantic import ValidationError
|
|
||||||
from sqlalchemy.orm import Session
|
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.db.session import SessionLocal
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.token import TokenPayload
|
|
||||||
from app.services.user import get_user
|
|
||||||
|
|
||||||
|
|
||||||
def get_db() -> Generator:
|
def get_db() -> Generator:
|
||||||
@ -24,59 +17,56 @@ def get_db() -> Generator:
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
def get_current_user(
|
def get_default_user(db: Session = Depends(get_db)) -> User:
|
||||||
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
|
|
||||||
) -> 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:
|
# Try to get the first user
|
||||||
payload = jwt.decode(
|
user = db.query(User).filter(User.is_superuser).first()
|
||||||
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"},
|
|
||||||
)
|
|
||||||
|
|
||||||
user = get_user(db, user_id=token_data.sub)
|
# If no user exists, create a default superuser
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
# Import here to avoid circular imports
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
from app.core.security import get_password_hash
|
||||||
detail="User not found",
|
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
|
return user
|
||||||
|
|
||||||
|
|
||||||
def get_current_active_user(
|
def get_current_user(db: Session = Depends(get_db)) -> User:
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
) -> User:
|
|
||||||
"""
|
"""
|
||||||
Dependency to get the current active user.
|
Dependency to get the current user without authentication.
|
||||||
"""
|
"""
|
||||||
if not current_user.is_active:
|
return get_default_user(db)
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Inactive user",
|
|
||||||
)
|
|
||||||
|
|
||||||
return current_user
|
|
||||||
|
|
||||||
|
|
||||||
def get_current_active_superuser(
|
def get_current_active_user(db: Session = Depends(get_db)) -> User:
|
||||||
current_user: User = Depends(get_current_active_user),
|
|
||||||
) -> User:
|
|
||||||
"""
|
"""
|
||||||
Dependency to get the current active superuser.
|
Dependency to get the current active user without authentication.
|
||||||
"""
|
"""
|
||||||
if not current_user.is_superuser:
|
return get_default_user(db)
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="The user doesn't have enough privileges",
|
def get_current_active_superuser(db: Session = Depends(get_db)) -> User:
|
||||||
)
|
"""
|
||||||
|
Dependency to get the current active superuser without authentication.
|
||||||
return current_user
|
"""
|
||||||
|
return get_default_user(db)
|
@ -34,7 +34,6 @@ async def get_news(
|
|||||||
categories: Optional[str] = None,
|
categories: Optional[str] = None,
|
||||||
countries: Optional[str] = None,
|
countries: Optional[str] = None,
|
||||||
languages: Optional[str] = None,
|
languages: Optional[str] = None,
|
||||||
current_user: Optional[User] = Depends(deps.get_current_active_user),
|
|
||||||
refresh: bool = False,
|
refresh: bool = False,
|
||||||
background_tasks: BackgroundTasks = None,
|
background_tasks: BackgroundTasks = None,
|
||||||
):
|
):
|
||||||
@ -97,9 +96,9 @@ async def get_personalized_news(
|
|||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 25,
|
limit: int = 25,
|
||||||
current_user: User = Depends(deps.get_current_active_user),
|
|
||||||
refresh: bool = False,
|
refresh: bool = False,
|
||||||
background_tasks: BackgroundTasks = None,
|
background_tasks: BackgroundTasks = None,
|
||||||
|
current_user: User = Depends(deps.get_current_user),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retrieve news articles based on the user's preferences.
|
Retrieve news articles based on the user's preferences.
|
||||||
@ -169,7 +168,6 @@ async def refresh_news(
|
|||||||
countries: Optional[str] = None,
|
countries: Optional[str] = None,
|
||||||
languages: Optional[str] = None,
|
languages: Optional[str] = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
current_user: User = Depends(deps.get_current_active_user),
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Fetch fresh news from Mediastack API and return them.
|
Fetch fresh news from Mediastack API and return them.
|
||||||
@ -216,7 +214,7 @@ async def get_saved_articles(
|
|||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 100,
|
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.
|
Get articles saved by the current user.
|
||||||
@ -232,7 +230,7 @@ async def save_article(
|
|||||||
*,
|
*,
|
||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
article_in: SavedArticleCreate,
|
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.
|
Save an article for the current user.
|
||||||
@ -271,7 +269,7 @@ async def save_article(
|
|||||||
async def remove_saved_article(
|
async def remove_saved_article(
|
||||||
saved_article_id: int,
|
saved_article_id: int,
|
||||||
db: Session = Depends(deps.get_db),
|
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.
|
Remove a saved article for the current user.
|
||||||
|
@ -1,80 +1,39 @@
|
|||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api import deps
|
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.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.news import UserPreference as UserPreferenceSchema, UserPreferenceUpdate
|
||||||
from app.schemas.token import Token
|
|
||||||
from app.services.user import (
|
from app.services.user import (
|
||||||
authenticate_user,
|
|
||||||
create_user,
|
|
||||||
update_user,
|
|
||||||
get_user_by_email,
|
get_user_by_email,
|
||||||
get_user,
|
get_user,
|
||||||
get_user_preference,
|
get_user_preference,
|
||||||
update_user_preference,
|
update_user_preference,
|
||||||
|
update_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/token", response_model=Token)
|
@router.get("/default", response_model=UserSchema)
|
||||||
async def login_for_access_token(
|
async def get_default_user(
|
||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
||||||
) -> Any:
|
) -> 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)
|
return deps.get_default_user(db)
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me", response_model=UserSchema)
|
@router.get("/me", response_model=UserSchema)
|
||||||
async def read_users_me(
|
async def read_users_me(
|
||||||
current_user: User = Depends(deps.get_current_active_user),
|
current_user: User = Depends(deps.get_current_user),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Get current user.
|
Get current user (default user without authentication).
|
||||||
"""
|
"""
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
@ -84,10 +43,10 @@ async def update_user_me(
|
|||||||
*,
|
*,
|
||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
user_in: UserUpdate,
|
user_in: UserUpdate,
|
||||||
current_user: User = Depends(deps.get_current_active_user),
|
current_user: User = Depends(deps.get_current_user),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Update current user.
|
Update current user (default user without authentication).
|
||||||
"""
|
"""
|
||||||
if user_in.email and user_in.email != current_user.email:
|
if user_in.email and user_in.email != current_user.email:
|
||||||
if get_user_by_email(db, user_in.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)
|
@router.get("/me/preferences", response_model=UserPreferenceSchema)
|
||||||
async def read_user_preferences(
|
async def read_user_preferences(
|
||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
current_user: User = Depends(deps.get_current_active_user),
|
current_user: User = Depends(deps.get_current_user),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Get current user's preferences.
|
Get current user's preferences (default user without authentication).
|
||||||
"""
|
"""
|
||||||
preferences = get_user_preference(db, current_user.id)
|
preferences = get_user_preference(db, current_user.id)
|
||||||
if not preferences:
|
if not preferences:
|
||||||
@ -122,10 +81,10 @@ async def update_user_preferences(
|
|||||||
*,
|
*,
|
||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
preferences_in: UserPreferenceUpdate,
|
preferences_in: UserPreferenceUpdate,
|
||||||
current_user: User = Depends(deps.get_current_active_user),
|
current_user: User = Depends(deps.get_current_user),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Update current user's preferences.
|
Update current user's preferences (default user without authentication).
|
||||||
"""
|
"""
|
||||||
preferences = update_user_preference(
|
preferences = update_user_preference(
|
||||||
db,
|
db,
|
||||||
@ -139,16 +98,14 @@ async def update_user_preferences(
|
|||||||
return preferences
|
return preferences
|
||||||
|
|
||||||
|
|
||||||
# Admin endpoints
|
|
||||||
@router.get("/", response_model=List[UserSchema])
|
@router.get("/", response_model=List[UserSchema])
|
||||||
async def read_users(
|
async def read_users(
|
||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
current_user: User = Depends(deps.get_current_active_superuser),
|
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Retrieve users. Only for superusers.
|
Retrieve all users.
|
||||||
"""
|
"""
|
||||||
users = db.query(User).offset(skip).limit(limit).all()
|
users = db.query(User).offset(skip).limit(limit).all()
|
||||||
return users
|
return users
|
||||||
@ -158,10 +115,9 @@ async def read_users(
|
|||||||
async def read_user(
|
async def read_user(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
db: Session = Depends(deps.get_db),
|
db: Session = Depends(deps.get_db),
|
||||||
current_user: User = Depends(deps.get_current_active_superuser),
|
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Get a specific user by id. Only for superusers.
|
Get a specific user by id.
|
||||||
"""
|
"""
|
||||||
user = get_user(db, user_id=user_id)
|
user = get_user(db, user_id=user_id)
|
||||||
if not user:
|
if not user:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user