7 Commits
Author SHA1 Message Date
Esa Kataja 21e30b0fb2 Add more logging 2025-05-13 19:48:37 +03:00
Esa Kataja 07d8e15925 Update lock file to rc2 2025-05-10 15:19:17 +03:00
Esa Kataja 7609159f60 Add message model 2025-05-10 15:18:26 +03:00
Esa Kataja c2884ac9ce Add more logging 2025-05-10 15:17:57 +03:00
Esa Kataja 40010c9860 Change logger loglevel operation 2025-05-10 15:17:42 +03:00
Esa Kataja d97a665a9c Prepare 1.0rc2 release 2025-05-10 14:33:35 +03:00
Esa Kataja 39da976ef3 Add more comprehensive logging 2025-05-10 14:28:34 +03:00
12 changed files with 261 additions and 79 deletions
+19
View File
@@ -0,0 +1,19 @@
# Eurovision 25 Backend Changelog
## 1.0rc2 (2025-05-10)
### Changes
- Updated application version to 1.0rc2
- Added more comprehensive logging throughout the application
## 1.0rc1 (Previous Release)
### Features
- Initial release candidate
- Eurovision 25 Homereview API implementation
- Authentication and authorization system
- User management features
- Song management
- Review system
- Results calculation
+4
View File
@@ -220,6 +220,10 @@ Automated tests are not implemented at this time. Manual testing via the API doc
## TODOs ## TODOs
* [ ] Replace password hashing with passlib * [ ] Replace password hashing with passlib
* [ ] Switch to PostgreSQL from DuckDB
* [x] Implement more comprehensive logging
* [ ] Implement user disabling functionality
* [ ] Implement user password change functionality
## Contributing ## Contributing
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "Eurovision-25-backend" name = "Eurovision-25-backend"
version = "1.0rc1" version = "1.0rc2"
description = "Backend for Eurovision 25" description = "Backend for Eurovision 25"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+10 -1
View File
@@ -12,9 +12,18 @@ from lib.db import init_db
app = FastAPI( app = FastAPI(
title="Eurovision 25 Homereview API", title="Eurovision 25 Homereview API",
description="Backend API for Eurovision 25 Homereview application", description="Backend API for Eurovision 25 Homereview application",
version="1.0rc1", version="1.0rc2",
) )
@app.middleware("http")
async def log_requests(request, call_next):
body = await request.body()
logger.debug(f"Request body: {body.decode('utf-8')}")
response = await call_next(request)
return response
# Add CORS middleware # Add CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
+4
View File
@@ -6,6 +6,7 @@ from dotenv import load_dotenv
import os import os
from lib.helpers import hash_password from lib.helpers import hash_password
from lib.logger import logger
load_dotenv() load_dotenv()
@@ -61,6 +62,9 @@ def seed_db() -> None:
# Seed Admin users # Seed Admin users
admin_username = os.getenv("ADMIN_USERNAME") admin_username = os.getenv("ADMIN_USERNAME")
hashed_admin_password = hash_password(os.getenv("ADMIN_PASSWORD", "password")) hashed_admin_password = hash_password(os.getenv("ADMIN_PASSWORD", "password"))
logger.debug(
f"Admin user: {admin_username}, hashed password: {hashed_admin_password}"
)
with get_connection() as conn: with get_connection() as conn:
conn.execute( conn.execute(
'INSERT INTO "User" (username, hashed_password, team_id, is_active, is_admin) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO "User" (username, hashed_password, team_id, is_active, is_admin) VALUES (?, ?, ?, ?, ?)',
+17 -7
View File
@@ -1,16 +1,26 @@
from loguru import logger from loguru import logger
from pathlib import Path from pathlib import Path
import sys import sys
from dotenv import load_dotenv
import os
LOG_PATH = Path("./data/logs/logs.log").absolute() load_dotenv()
ERROR_PATH = Path("./data/logs/errors.log").absolute()
LOG_PATH = Path(os.getenv("LOG_PATH", "./data/logs/logs.log")).absolute()
ERROR_PATH = Path(os.getenv("ERROR_PATH", "./data/logs/errors.log")).absolute()
logger.remove() logger.remove()
logger.add(LOG_PATH, rotation="10 MB", compression="gz", level="INFO") logger.add(
logger.add(sys.stdout, level="INFO") LOG_PATH, rotation="10 MB", compression="gz", level=os.getenv("LOG_LEVEL", "INFO")
)
logger.add(sys.stdout, level=os.getenv("LOG_LEVEL", "INFO"))
logger.add(ERROR_PATH, rotation="10 MB", compression="gz", level="ERROR") logger.add(
logger.add(sys.stderr, level="ERROR") ERROR_PATH,
logger.add(sys.stdout, level="DEBUG") rotation="10 MB",
compression="gz",
level=os.getenv("LOG_LEVEL_ERROR", "ERROR"),
)
logger.add(sys.stderr, level=os.getenv("LOG_LEVEL_ERROR", "ERROR"))
logger = logger logger = logger
+8
View File
@@ -0,0 +1,8 @@
from pydantic import BaseModel, Field
from datetime import datetime
class Message(BaseModel):
type: str | None = None
message: str
timestamp: datetime = Field(default_factory=datetime.now)
+57 -19
View File
@@ -1,9 +1,10 @@
from fastapi import APIRouter, Body, HTTPException from fastapi import APIRouter, Body, HTTPException, Request
from models.user import CreateUser from models.user import CreateUser
from models.team import TeamBase, Team from models.team import TeamBase, Team
from lib.db import get_connection from lib.db import get_connection
from lib.helpers import hash_password from lib.helpers import hash_password
from lib.logger import logger
router = APIRouter(prefix="/admin", tags=["Admin"]) router = APIRouter(prefix="/admin", tags=["Admin"])
@@ -19,36 +20,73 @@ router = APIRouter(prefix="/admin", tags=["Admin"])
@router.post("/teams") @router.post("/teams")
async def create_team(team: TeamBase): async def create_team(team: TeamBase, request: Request):
with get_connection() as conn: logger.info(
conn.execute("INSERT INTO Team (name) VALUES (?)", (team.name,)) f"Admin action: Creating new team '{team.name}' from IP: {request.client.host}"
return {"message": "Team created successfully"} )
try:
with get_connection() as conn:
conn.execute("INSERT INTO Team (name) VALUES (?)", (team.name,))
logger.info(f"Team '{team.name}' created successfully")
return {"message": "Team created successfully"}
except Exception as e:
logger.error(f"Failed to create team '{team.name}': {str(e)}")
raise HTTPException(status_code=500, detail="Failed to create team")
@router.get("/teams", response_model=list[Team]) @router.get("/teams", response_model=list[Team])
async def list_teams(): async def list_teams(request: Request):
logger.debug(f"Admin action: Listing all teams from IP: {request.client.host}")
with get_connection() as conn: with get_connection() as conn:
teams = conn.execute("SELECT * FROM Team").fetchdf() teams = conn.execute("SELECT * FROM Team").fetchdf()
return teams.to_dict(orient="records") return teams.to_dict(orient="records")
@router.post("/users") @router.post("/users")
async def create_user(user: CreateUser): async def create_user(user: CreateUser, request: Request):
with get_connection() as conn: logger.info(
conn.execute( f"Admin action: Creating new user '{user.username}' from IP: {request.client.host}"
"INSERT INTO User (username, hashed_password, team_id, is_admin) VALUES (?, ?, ?, ?)", )
( try:
user.username, with get_connection() as conn:
hash_password(user.password), # Check if username already exists
user.team_id, existing = conn.execute(
user.is_admin, "SELECT COUNT(*) as count FROM User WHERE username = ?",
), (user.username,),
).fetchdf()
if existing["count"][0] > 0:
logger.warning(
f"Failed to create user: Username '{user.username}' already exists"
)
raise HTTPException(status_code=400, detail="Username already exists")
conn.execute(
"INSERT INTO User (username, hashed_password, team_id, is_admin) VALUES (?, ?, ?, ?)",
(
user.username,
hash_password(user.password),
user.team_id,
user.is_admin,
),
)
logger.info(
f"User '{user.username}' created successfully with team_id: {user.team_id}, admin status: {user.is_admin}"
) )
return {"message": "User created successfully"} return {"message": "User created successfully"}
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
logger.error(f"Failed to create user '{user.username}': {str(e)}")
raise HTTPException(status_code=500, detail="Failed to create user")
@router.post("/users/") @router.post("/users/disable")
async def disable_user(user_id: int = Body(..., embed=True)): async def disable_user(user_id: int = Body(..., embed=True), request: Request = None):
logger.info(
f"Admin action: Attempting to disable user with ID: {user_id} from IP: {request.client.host}"
)
logger.warning(f"Disable user functionality not implemented for user_id: {user_id}")
raise HTTPException(status_code=405, detail="Method not yet implemented") raise HTTPException(status_code=405, detail="Method not yet implemented")
+21 -6
View File
@@ -1,24 +1,34 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException, Request
from lib.db import get_connection from lib.db import get_connection
from models.user import User, UserLogin from models.user import User, UserLogin
from lib.helpers import verify_password from lib.helpers import verify_password
from lib.logger import logger
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/token", response_model=User) @router.post("/token", response_model=User)
async def login(user_login: UserLogin): async def login(user_login: UserLogin, request: Request):
logger.debug(
f"Login attempt for user: {user_login.username} from IP: {request.client.host}"
)
with get_connection() as conn: with get_connection() as conn:
user_df = conn.execute( user_df = conn.execute(
'SELECT * FROM "User" WHERE username = ?', (user_login.username,) 'SELECT * FROM "User" WHERE username = ?', (user_login.username,)
).fetchdf() ).fetchdf()
if user_df.empty: if user_df.empty:
logger.warning(
f"Failed login: Username {user_login.username} not found - IP: {request.client.host}"
)
raise HTTPException(status_code=401, detail="Invalid username or password") raise HTTPException(status_code=401, detail="Invalid username or password")
user_data = user_df.to_dict(orient="records")[0] user_data = user_df.to_dict(orient="records")[0]
if not verify_password(user_login.password, user_data["hashed_password"]): if not verify_password(user_login.password, user_data["hashed_password"]):
logger.warning(
f"Failed login: Incorrect password for user {user_login.username} - IP: {request.client.host}"
)
raise HTTPException(status_code=401, detail="Invalid username or password") raise HTTPException(status_code=401, detail="Invalid username or password")
# Update the login timestamp in the database # Update the login timestamp in the database
@@ -29,9 +39,14 @@ async def login(user_login: UserLogin):
) )
# Fetch the updated user data with the new last_login timestamp # Fetch the updated user data with the new last_login timestamp
updated_user = conn.execute( updated_user = (
'SELECT * FROM "User" WHERE id = ?', conn.execute('SELECT * FROM "User" WHERE id = ?', (user_data["id"],))
(user_data["id"],) .fetchdf()
).fetchdf().to_dict(orient="records")[0] .to_dict(orient="records")[0]
)
logger.info(
f"Successful login: User {user_login.username} (ID: {user_data['id']}) logged in from {request.client.host}"
)
return User(**updated_user) return User(**updated_user)
+101 -37
View File
@@ -1,7 +1,9 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from models.review import ReviewOut, ReviewSearch, ReviewIn from models.review import ReviewOut, ReviewIn
from models.msg import Message
from lib.db import get_connection from lib.db import get_connection
from lib.logger import logger
router = APIRouter(prefix="/reviews", tags=["reviews"]) router = APIRouter(prefix="/reviews", tags=["reviews"])
@@ -16,52 +18,114 @@ async def list_reviews():
@router.get("/", response_model=ReviewOut) @router.get("/", response_model=ReviewOut)
async def get_review(review_id: ReviewSearch): async def get_review(song_id: int, user_id: int):
with get_connection() as conn: with get_connection() as conn:
review = conn.execute( review = conn.execute(
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?", "SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
(review_id.song_id, review_id.user_id), (song_id, user_id),
).fetchdf() ).fetchdf()
if review.empty: if review.empty:
raise HTTPException(status_code=404, detail="Review not found") raise HTTPException(status_code=404, detail="Review not found")
return review.to_dict(orient="records")[0] return review.to_dict(orient="records")[0]
@router.post("/", response_model=ReviewOut) @router.post("/", response_model=Message)
async def create_review(review: ReviewIn): async def create_review(review: ReviewIn):
with get_connection() as conn: logger.debug(f"Received create review request with data: {review.dict()}")
review = conn.execute( try:
"INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) VALUES (?, ?, ?, ?, ?, ?)", with get_connection() as conn:
( # Log the SQL query and parameters
review.user_id, logger.debug(
review.song_id, f"Executing INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) "
review.score_song, f"VALUES ({review.user_id}, {review.song_id}, {review.score_song}, "
review.score_show, f"{review.score_show}, {review.score_costume}, '{review.text_review}')"
review.score_costume, )
review.text_review,
), # Insert the review
).fetchdf() conn.execute(
if review.empty: "INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) VALUES (?, ?, ?, ?, ?, ?)",
raise HTTPException(status_code=404, detail="Review not found") (
return review.to_dict(orient="records")[0] review.user_id,
review.song_id,
review.score_song,
review.score_show,
review.score_costume,
review.text_review,
),
)
# Fetch the newly created review
logger.debug(
f"Checking if review was created for song_id={review.song_id}, user_id={review.user_id}"
)
result = conn.execute(
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
(review.song_id, review.user_id),
).fetchdf()
logger.debug(f"Database result for newly created review: {result}")
if result.empty:
logger.warning(
f"Review not found after insert attempt: song_id={review.song_id}, user_id={review.user_id}"
)
raise HTTPException(status_code=404, detail="Review not found")
logger.info(
f"Review created successfully for song_id={review.song_id}, user_id={review.user_id}"
)
return Message(type="success", message="Review created successfully")
except Exception as e:
logger.error(f"Error creating review: {str(e)}")
raise
@router.put("/", response_model=ReviewOut) @router.put("/", response_model=Message)
async def update_review(review: ReviewIn): async def update_review(review: ReviewIn):
with get_connection() as conn: print("update_review")
review = conn.execute( logger.debug(f"Received update review request with data: {review.dict()}")
"UPDATE Review SET user_id = ?, song_id = ?, score_song = ?, score_show = ?, score_costume = ?, text_review = ? WHERE song_id = ? AND user_id = ?", try:
( with get_connection() as conn:
review.user_id, # Log the SQL query and parameters
review.song_id, logger.debug(
review.score_song, f"Executing UPDATE Review SET score_song = {review.score_song}, "
review.score_show, f"score_show = {review.score_show}, score_costume = {review.score_costume}, "
review.score_costume, f"text_review = '{review.text_review}' WHERE song_id = {review.song_id} "
review.text_review, f"AND user_id = {review.user_id}"
review.song_id, )
review.user_id,
), # Update the review
).fetchdf() conn.execute(
if review.empty: "UPDATE Review SET score_song = ?, score_show = ?, score_costume = ?, text_review = ? WHERE song_id = ? AND user_id = ?",
raise HTTPException(status_code=404, detail="Review not found") (
return review.to_dict(orient="records")[0] review.score_song,
review.score_show,
review.score_costume,
review.text_review,
review.song_id,
review.user_id,
),
)
# Fetch the updated review
logger.debug(
f"Checking if review was updated for song_id={review.song_id}, user_id={review.user_id}"
)
result = conn.execute(
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
(review.song_id, review.user_id),
).fetchdf()
logger.debug(f"Database result: {result}")
if result.empty:
logger.warning(
f"Review not found after update attempt: song_id={review.song_id}, user_id={review.user_id}"
)
raise HTTPException(status_code=404, detail="Review not found")
logger.info(
f"Review updated successfully for song_id={review.song_id}, user_id={review.user_id}"
)
return Message(type="success", message="Review updated successfully")
except Exception as e:
logger.error(f"Error updating review: {str(e)}")
raise
+16 -5
View File
@@ -1,14 +1,15 @@
from fastapi import APIRouter from fastapi import APIRouter, HTTPException, Request
from fastapi import HTTPException
from models.user import User, UserChangePassword from models.user import User, UserChangePassword
from lib.db import get_connection from lib.db import get_connection
from lib.logger import logger
router = APIRouter(prefix="/users", tags=["users"]) router = APIRouter(prefix="/users", tags=["users"])
@router.get("/", response_model=list[User]) @router.get("/", response_model=list[User])
async def list_users(): async def list_users(request: Request):
logger.info(f"User list requested from {request.client.host}")
with get_connection() as conn: with get_connection() as conn:
users = conn.execute('SELECT * FROM "User"').fetchdf() users = conn.execute('SELECT * FROM "User"').fetchdf()
@@ -18,19 +19,29 @@ async def list_users():
@router.get("/{user_id}", response_model=User) @router.get("/{user_id}", response_model=User)
async def get_user(user_id: int): async def get_user(user_id: int, request: Request):
logger.debug(
f"User details requested for user_id: {user_id} from {request.client.host}"
)
with get_connection() as conn: with get_connection() as conn:
user_df = conn.execute( user_df = conn.execute(
'SELECT * FROM "User" WHERE id = ?', (user_id,) 'SELECT * FROM "User" WHERE id = ?', (user_id,)
).fetchdf() ).fetchdf()
if user_df.empty: if user_df.empty:
logger.warning(f"Failed user lookup: user_id {user_id} not found")
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
user_data = user_df.to_dict(orient="records")[0] user_data = user_df.to_dict(orient="records")[0]
logger.debug(f"User details retrieved: {user_data}")
return User(**user_data) return User(**user_data)
@router.patch("/{user_id}") @router.patch("/{user_id}")
async def change_password(user: UserChangePassword): async def change_password(user_id: int, user: UserChangePassword, request: Request):
logger.info(
f"Password change attempt for user_id: {user_id} from {request.client.host}"
)
# Implementation will go here when completed
logger.warning(f"Password change not implemented for user_id: {user_id}")
raise HTTPException(status_code=401, detail="Not implemented") raise HTTPException(status_code=401, detail="Not implemented")
Generated
+1 -1
View File
@@ -131,7 +131,7 @@ wheels = [
[[package]] [[package]]
name = "eurovision-25-backend" name = "eurovision-25-backend"
version = "0.1.0" version = "1.0rc2"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },