Merge pull request 'Added API key requirement to main paths and support for different users' (#1) from feature/user-id-support into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
commit
fc15dc91d0
5 files changed
+89
-42
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE `seadex_state` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint(20) NOT NULL,
|
||||||
|
`anilist_id` int(10) unsigned NOT NULL,
|
||||||
|
`anime_name` varchar(255) NOT NULL,
|
||||||
|
`last_updated` varchar(40) DEFAULT NULL,
|
||||||
|
`active` tinyint(1) NOT NULL DEFAULT '1',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_user_anilist` (`user_id`,`anilist_id`),
|
||||||
|
CONSTRAINT `fk_seadex_state_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
|
||||||
|
)
|
||||||
+2
-1
@@ -12,8 +12,9 @@ httpcore==1.0.9
|
|||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
idna==3.20
|
idna==3.20
|
||||||
lib-db-module==0.3.0
|
lib-db-module==0.3.0
|
||||||
lib-secret-manager==0.3.1
|
lib-secret-manager==0.3.6
|
||||||
lib-send-notifications==0.1.0
|
lib-send-notifications==0.1.0
|
||||||
|
lib-service-auth==0.1.0
|
||||||
msgspec==0.22.0
|
msgspec==0.22.0
|
||||||
mysql-connector-python==9.4.0
|
mysql-connector-python==9.4.0
|
||||||
opentelemetry-api==1.45.1
|
opentelemetry-api==1.45.1
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from service_auth import KeySource, ServiceAuth
|
||||||
|
|
||||||
|
|
||||||
|
class Caller(StrEnum):
|
||||||
|
GATEWAY = "gateway"
|
||||||
|
SCHEDULER = "scheduler"
|
||||||
|
|
||||||
|
auth = ServiceAuth({
|
||||||
|
Caller.GATEWAY: KeySource("GATEWAY_KEY", "/etc/secrets/gateway_key"),
|
||||||
|
Caller.SCHEDULER: KeySource("SCHEDULER_KEY", "/etc/secrets/scheduler_key"),
|
||||||
|
})
|
||||||
+57
-40
@@ -4,11 +4,13 @@ from contextlib import asynccontextmanager
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
|
||||||
from pydantic import BaseModel, Field, computed_field
|
from pydantic import BaseModel, Field, computed_field
|
||||||
from seadex import EntryNotFoundError, SeaDexEntry
|
from seadex import EntryNotFoundError, SeaDexEntry
|
||||||
|
from service_auth import UserId
|
||||||
|
|
||||||
from anilist_query import AniListError, AnimeNotFoundError, MissingAnimeName, get_anime_name
|
from anilist_query import AniListError, AnimeNotFoundError, MissingAnimeName, get_anime_name
|
||||||
|
from auth_config import Caller, auth
|
||||||
from db_module import (
|
from db_module import (
|
||||||
close_connection_pool,
|
close_connection_pool,
|
||||||
create_connection_pool,
|
create_connection_pool,
|
||||||
@@ -70,9 +72,13 @@ api = FastAPI(
|
|||||||
title="Service SeaDex",
|
title="Service SeaDex",
|
||||||
description="Queries SeaDex for new torrent information",
|
description="Queries SeaDex for new torrent information",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
lifespan=lifespan
|
lifespan=lifespan,
|
||||||
|
dependencies=[Depends(auth.authenticate)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_router = APIRouter(dependencies=[Depends(auth.require(Caller.GATEWAY))])
|
||||||
|
scheduler_router = APIRouter(dependencies=[Depends(auth.require(Caller.SCHEDULER))])
|
||||||
|
|
||||||
@api.middleware("http")
|
@api.middleware("http")
|
||||||
async def prometheus_middleware(request: Request, call_next):
|
async def prometheus_middleware(request: Request, call_next):
|
||||||
logger.debug(f"[Metrics] Incoming request: {request.method} {request.url.path}")
|
logger.debug(f"[Metrics] Incoming request: {request.method} {request.url.path}")
|
||||||
@@ -101,22 +107,25 @@ def check_for_update(seadex: SeaDexEntry, anilist_id: int) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@api.post("/update")
|
@scheduler_router.post("/update")
|
||||||
def check_seadex(request:Request, db=Depends(get_db)):
|
def check_seadex(request:Request, db=Depends(get_db)):
|
||||||
results = {"checked": 0, "new": 0, "errors": 0}
|
results = {"checked": 0, "new": 0, "errors": 0}
|
||||||
|
|
||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT id, anilist_id, anime_name, last_updated "
|
"SELECT id, anilist_id, anime_name, last_updated, user_id "
|
||||||
"FROM seadex_state WHERE active = 1"
|
"FROM seadex_state WHERE active = 1"
|
||||||
)
|
)
|
||||||
anime = cursor.fetchall()
|
anime = cursor.fetchall()
|
||||||
|
|
||||||
|
cache: dict[int, str | None] = {}
|
||||||
with SeaDexEntry() as seadex:
|
with SeaDexEntry() as seadex:
|
||||||
for row_id, anilist_id, anime_name, last_updated in anime:
|
for row_id, anilist_id, anime_name, last_updated, user_id in anime:
|
||||||
results["checked"] += 1
|
results["checked"] += 1
|
||||||
try:
|
try:
|
||||||
current = check_for_update(seadex, anilist_id)
|
if anilist_id not in cache:
|
||||||
|
cache[anilist_id] = check_for_update(seadex, anilist_id)
|
||||||
|
current = cache[anilist_id]
|
||||||
|
|
||||||
if current is None:
|
if current is None:
|
||||||
logger.debug(f"[FastAPI] No entry yet for {anime_name} on SeaDex")
|
logger.debug(f"[FastAPI] No entry yet for {anime_name} on SeaDex")
|
||||||
@@ -124,31 +133,32 @@ def check_seadex(request:Request, db=Depends(get_db)):
|
|||||||
|
|
||||||
if current != last_updated:
|
if current != last_updated:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[FastAPI] {anime_name} updated, Anilist ID: {anilist_id}"
|
f"[FastAPI] {anime_name} updated, Anilist ID: {anilist_id}, User ID: {user_id}"
|
||||||
)
|
)
|
||||||
request.app.state.notifier.send(
|
request.app.state.notifier.send(
|
||||||
title=anime_name,
|
title=anime_name,
|
||||||
body="SeaDex entry updated",
|
body="SeaDex entry updated",
|
||||||
link=f"https://seadex.moe/{anilist_id}/",
|
link=f"https://seadex.moe/{anilist_id}/",
|
||||||
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE seadex_state SET last_updated = %s WHERE id = %s",
|
"UPDATE seadex_state SET last_updated = %s WHERE id = %s AND user_id = %s",
|
||||||
(current, row_id),
|
(current, row_id, user_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[FastAPI] Notified and updated db for id={row_id}, "
|
f"[FastAPI] Notified and updated db for id={row_id}, "
|
||||||
f"Anilist ID={anilist_id}"
|
f"Anilist ID={anilist_id}, User ID: {user_id}"
|
||||||
)
|
)
|
||||||
results["new"] += 1
|
results["new"] += 1
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
results["errors"] += 1
|
results["errors"] += 1
|
||||||
logger.exception(f"[FastAPI] Failed for id={row_id} Anilist={anilist_id}",)
|
logger.exception(f"[FastAPI] Failed for id={row_id} Anilist={anilist_id}, User ID: {user_id}",)
|
||||||
|
|
||||||
if results["errors"]:
|
if results["errors"]:
|
||||||
logger.warning(f"[FastAPI] SeaDex check finished with errors: {results}")
|
logger.warning(f"[FastAPI] SeaDex check finished with errors: {results}")
|
||||||
@@ -157,9 +167,9 @@ def check_seadex(request:Request, db=Depends(get_db)):
|
|||||||
logger.info(f"[FastAPI] SeaDex check complete: {results}")
|
logger.info(f"[FastAPI] SeaDex check complete: {results}")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@api.post("/add/anime")
|
@user_router.post("/add/anime")
|
||||||
def add_anime(anime: AnimeCreate, db=Depends(get_db)):
|
def add_anime(anime: AnimeCreate, user_id: UserId, db=Depends(get_db)):
|
||||||
logger.info(f"[FastAPI] Received request to track anilist_id={anime.anilist_id}")
|
logger.info(f"[FastAPI] Received request to track anilist_id={anime.anilist_id}, user_id={user_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
anime_name = get_anime_name(anime.anilist_id)
|
anime_name = get_anime_name(anime.anilist_id)
|
||||||
@@ -175,16 +185,16 @@ def add_anime(anime: AnimeCreate, db=Depends(get_db)):
|
|||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT id FROM seadex_state WHERE anilist_id = %s",
|
"SELECT id FROM seadex_state WHERE anilist_id = %s AND user_id = %s",
|
||||||
(anime.anilist_id,),
|
(anime.anilist_id, user_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
if cursor.fetchone():
|
if cursor.fetchone():
|
||||||
logger.debug("[FastAPI] Found existing entry, updating name and setting to active")
|
logger.debug("[FastAPI] Found existing entry, updating name and setting to active")
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE seadex_state SET anime_name = %s, active = 1 "
|
"UPDATE seadex_state SET anime_name = %s, active = 1 "
|
||||||
"WHERE anilist_id = %s",
|
"WHERE anilist_id = %s AND user_id=%s",
|
||||||
(anime_name, anime.anilist_id),
|
(anime_name, anime.anilist_id, user_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {
|
return {
|
||||||
@@ -195,12 +205,12 @@ def add_anime(anime: AnimeCreate, db=Depends(get_db)):
|
|||||||
|
|
||||||
with SeaDexEntry() as seadex:
|
with SeaDexEntry() as seadex:
|
||||||
current = check_for_update(seadex, anime.anilist_id)
|
current = check_for_update(seadex, anime.anilist_id)
|
||||||
logger.debug("[FastAPI] Inserting new anime with last_updated={current}")
|
logger.debug(f"[FastAPI] Inserting new anime with last_updated={current}")
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO seadex_state (anilist_id, anime_name, last_updated) "
|
"INSERT INTO seadex_state (anilist_id, anime_name, last_updated, user_id) "
|
||||||
"VALUES (%s, %s, %s)",
|
"VALUES (%s, %s, %s, %s)",
|
||||||
(anime.anilist_id, anime_name, current),
|
(anime.anilist_id, anime_name, current, user_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {
|
return {
|
||||||
@@ -211,27 +221,27 @@ def add_anime(anime: AnimeCreate, db=Depends(get_db)):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logger.exception(f"[FastAPI] Failed for anilist_id={anime.anilist_id} name={anime_name}")
|
logger.exception(f"[FastAPI] Failed for anilist_id={anime.anilist_id}, name={anime_name}, user_id={user_id}")
|
||||||
raise HTTPException(status_code=500) from e
|
raise HTTPException(status_code=500) from e
|
||||||
|
|
||||||
@api.post("/delete/anime")
|
@user_router.post("/delete/anime")
|
||||||
def remove_anime(anime: AnimeCreate, db=Depends(get_db)):
|
def remove_anime(anime: AnimeCreate, user_id: UserId, db=Depends(get_db)):
|
||||||
logger.info(f"[FastAPI] Received request to deactivate tracking for anilist_id={anime.anilist_id}")
|
logger.info(f"[FastAPI] Received request to deactivate tracking for anilist_id={anime.anilist_id}, user_id={user_id}")
|
||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT active FROM seadex_state WHERE anilist_id = %s",
|
"SELECT active FROM seadex_state WHERE anilist_id = %s AND user_id = %s",
|
||||||
(anime.anilist_id,),
|
(anime.anilist_id, user_id),
|
||||||
)
|
)
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if row and row[0]:
|
if row and row[0]:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE seadex_state SET active = 0 "
|
"UPDATE seadex_state SET active = 0 "
|
||||||
"WHERE anilist_id = %s",
|
"WHERE anilist_id = %s AND user_id = %s",
|
||||||
(anime.anilist_id,),
|
(anime.anilist_id, user_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
logger.debug(f"[FastAPI] Successfully deactivated tracking for anilist_id={anime.anilist_id}")
|
logger.debug(f"[FastAPI] Successfully deactivated tracking for anilist_id={anime.anilist_id}, user_id={user_id}")
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"detail": f"Deactivated entry {anime.anilist_id}",
|
"detail": f"Deactivated entry {anime.anilist_id}",
|
||||||
@@ -239,28 +249,30 @@ def remove_anime(anime: AnimeCreate, db=Depends(get_db)):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logger.exception(f"[FastAPI] Failed removeing tracking for id={anime.anilist_id}")
|
logger.exception(f"[FastAPI] Failed removeing tracking for id={anime.anilist_id}, user_id={user_id}")
|
||||||
raise HTTPException(status_code=500) from e
|
raise HTTPException(status_code=500) from e
|
||||||
|
|
||||||
if row is None:
|
if row is None:
|
||||||
logger.debug(f"[FastAPI] No tracked entry for anilist_id={anime.anilist_id}")
|
logger.debug(f"[FastAPI] No tracked entry for anilist_id={anime.anilist_id}, user_id={user_id}")
|
||||||
raise HTTPException(status_code=404, detail="No tracked entry for this AniList ID")
|
raise HTTPException(status_code=404, detail="No tracked entry for this AniList ID")
|
||||||
if not row[0]:
|
if not row[0]:
|
||||||
logger.debug(f"[FastAPI] Tracking was already deactivated for anilist_id={anime.anilist_id}")
|
logger.debug(f"[FastAPI] Tracking was already deactivated for anilist_id={anime.anilist_id}, user_id={user_id}")
|
||||||
return {"status": "ok", "detail": f"Entry {anime.anilist_id} was already inactive"}
|
return {"status": "ok", "detail": f"Entry {anime.anilist_id} was already inactive"}
|
||||||
|
|
||||||
@api.get("/list/anime", response_model=AnimeList)
|
@user_router.get("/list/anime", response_model=AnimeList)
|
||||||
def list_active_and_inactive_anime(db=Depends(get_db)):
|
def list_active_and_inactive_anime(user_id: UserId, db=Depends(get_db)):
|
||||||
logger.info(f"[FastAPI] Received request to list anime")
|
logger.info(f"[FastAPI] Received request to list anime for User ID = {user_id}")
|
||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT anime_name, active, last_updated " \
|
"SELECT anime_name, active, last_updated " \
|
||||||
"FROM seadex_state ORDER BY anime_name"
|
"FROM seadex_state WHERE user_id = %s "
|
||||||
|
"ORDER BY anime_name",
|
||||||
|
(user_id,),
|
||||||
)
|
)
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"[FastAPI] Failed to list anime")
|
logger.exception("[FastAPI] Failed to list anime")
|
||||||
raise HTTPException(status_code=500) from e
|
raise HTTPException(status_code=500) from e
|
||||||
|
|
||||||
anime_list=AnimeList ()
|
anime_list=AnimeList ()
|
||||||
@@ -272,11 +284,16 @@ def list_active_and_inactive_anime(db=Depends(get_db)):
|
|||||||
(anime_list.active if is_active else anime_list.inactive).append(entry)
|
(anime_list.active if is_active else anime_list.inactive).append(entry)
|
||||||
return anime_list
|
return anime_list
|
||||||
|
|
||||||
|
api.include_router(user_router)
|
||||||
|
api.include_router(scheduler_router)
|
||||||
|
|
||||||
async def start_servers():
|
async def start_servers():
|
||||||
logger.info("[Server] Starting main API (port 5000) and metrics server (port 9000)...")
|
logger.info("[Server] Starting main API (port 5000) and metrics server (port 9000)...")
|
||||||
logger.info(f"[Server] Build commit id={os.getenv("COMMIT_ID","unknown")}")
|
logger.info(f"[Server] Build commit id={os.getenv("COMMIT_ID","unknown")}")
|
||||||
|
|
||||||
|
auth.load_keys(api, Caller.GATEWAY, Caller.SCHEDULER)
|
||||||
|
cleanup_secret_files(auth.key_paths(Caller.GATEWAY, Caller.SCHEDULER))
|
||||||
|
|
||||||
config_main = uvicorn.Config(api, host="0.0.0.0", port=5000, log_level=LOG_LEVEL.lower(),log_config=None)
|
config_main = uvicorn.Config(api, host="0.0.0.0", port=5000, log_level=LOG_LEVEL.lower(),log_config=None)
|
||||||
config_metrics = uvicorn.Config(metrics_api, host="0.0.0.0", port=9000, log_level=LOG_LEVEL.lower(),log_config=None)
|
config_metrics = uvicorn.Config(metrics_api, host="0.0.0.0", port=9000, log_level=LOG_LEVEL.lower(),log_config=None)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
from fastapi import FastAPI, Response
|
from fastapi import FastAPI, Response
|
||||||
from prometheus_client import CONTENT_TYPE_LATEST, Counter, generate_latest
|
from prometheus_client import CONTENT_TYPE_LATEST, Counter, generate_latest
|
||||||
|
|
||||||
metrics_api = FastAPI(title="Metrics Server", description="Prometheus metrics endpoint")
|
metrics_api = FastAPI(
|
||||||
|
title="Metrics Server",
|
||||||
|
description="Prometheus metrics endpoint",
|
||||||
|
)
|
||||||
|
|
||||||
REQUEST_COUNTER = Counter("http_requests_total", "Total HTTP Requests", ["method", "endpoint", "status"])
|
REQUEST_COUNTER = Counter("http_requests_total", "Total HTTP Requests", ["method", "endpoint", "status"])
|
||||||
|
|
||||||
|
|
||||||
@metrics_api.get("/metrics")
|
@metrics_api.get("/metrics")
|
||||||
async def metrics():
|
async def metrics():
|
||||||
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
||||||
Reference in new issue
Block a user