start pre-4.8 cleanup

This commit is contained in:
Georges-Antoine Assi
2026-03-12 23:02:12 -04:00
parent 2beba3d141
commit 997e2c44aa
20 changed files with 164 additions and 83 deletions

View File

@@ -3,9 +3,9 @@ from typing import NotRequired, TypedDict
class TokenResponse(TypedDict):
access_token: str
refresh_token: NotRequired[str]
token_type: str
expires: int
refresh_token: NotRequired[str]
refresh_expires: NotRequired[int]

View File

@@ -315,60 +315,6 @@ async def get_task_by_id(request: Request, task_id: str) -> TaskStatusResponse:
return _build_task_status_response(job)
@protected_route(router.post, "/run", [Scope.TASKS_RUN])
async def run_all_tasks(request: Request) -> list[TaskExecutionResponse]:
"""Run all runnable tasks endpoint
Args:
request (Request): FastAPI Request object
Returns:
TaskExecutionResponse: Task execution response with details
"""
# Filter only runnable tasks
runnable_tasks = {
task["name"]: task["task"]
for task in manual_tasks + scheduled_tasks
if task["task"].enabled and task["task"].manual_run
}
if not runnable_tasks:
raise HTTPException(
status_code=400,
detail="No runnable tasks available to run",
)
jobs = [
(
task_name,
low_prio_queue.enqueue(
task_instance.run,
job_timeout=TASK_TIMEOUT,
result_ttl=TASK_RESULT_TTL,
meta={
"task_name": task_instance.title,
"task_type": task_instance.task_type.value,
},
),
)
for task_name, task_instance in runnable_tasks.items()
]
return [
TaskExecutionResponse(
task_name=task_name,
task_id=job.get_id(),
status=job.get_status() or JobStatus.QUEUED,
created_at=(
job.created_at.isoformat()
if job.created_at
else datetime.now(timezone.utc).isoformat()
),
enqueued_at=job.enqueued_at.isoformat() if job.enqueued_at else None,
)
for (task_name, job) in jobs
]
TASK_KWARGS = Body(default=None)

View File

@@ -141,7 +141,7 @@ def create_invite_link(
)
if expiration is not None and expiration <= 0:
msg = "expiration must be a positive integer"
msg = "Invite link expiration must be a positive integer"
log.error(msg)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,

View File

@@ -333,6 +333,7 @@ class OAuthHandler:
if not user.enabled:
raise UserDisabledException
return user, payload.claims
async def get_current_active_user_from_bearer_token(self, token: str):
@@ -415,9 +416,8 @@ class OpenIDHandler:
)
role = Role.VIEWER
claims_provided = False
if OIDC_CLAIM_ROLES and OIDC_CLAIM_ROLES in userinfo:
claims_provided = True
claims_provided = OIDC_CLAIM_ROLES and OIDC_CLAIM_ROLES in userinfo
if claims_provided:
roles = userinfo[OIDC_CLAIM_ROLES] or []
if OIDC_ROLE_ADMIN and OIDC_ROLE_ADMIN in roles:
role = Role.ADMIN

View File

@@ -69,6 +69,7 @@ class DBClientTokensHandler(DBBaseHandler):
stmt = delete(ClientToken).where(ClientToken.id == token_id)
if user_id is not None:
stmt = stmt.where(ClientToken.user_id == user_id)
result = session.execute(stmt.execution_options(synchronize_session="evaluate"))
return result.rowcount
@@ -82,11 +83,13 @@ class DBClientTokensHandler(DBBaseHandler):
token = session.get(ClientToken, token_id)
if token is None:
return
if (
token.last_used_at
and (now - to_utc(token.last_used_at)) < LAST_USED_DEBOUNCE
):
return
session.execute(
update(ClientToken)
.where(ClientToken.id == token_id)
@@ -108,11 +111,14 @@ class DBClientTokensHandler(DBBaseHandler):
.values(hashed_token=new_hash, last_used_at=None)
.execution_options(synchronize_session="evaluate")
)
if user_id is not None:
stmt = stmt.where(ClientToken.user_id == user_id)
result = session.execute(stmt)
if result.rowcount == 0:
return None
return session.get(ClientToken, token_id)
@begin_session
@@ -140,4 +146,5 @@ class DBClientTokensHandler(DBBaseHandler):
stmt = select(ClientToken).where(ClientToken.id == token_id)
if user_id is not None:
stmt = stmt.where(ClientToken.user_id == user_id)
return session.scalar(stmt)

View File

@@ -97,7 +97,7 @@ EJS_SUPPORTED_PLATFORMS = [
UPS.WONDERSWAN_COLOR,
]
OTHER_SUPPORTED_PLATFORMS = [
RUFFLE_SUPPORTED_PLATFORMS = [
UPS.BROWSER,
]
@@ -309,7 +309,7 @@ class DBRomsHandler(DBBaseHandler):
"""Filter based on whether the rom is playable on supported platforms."""
predicate = or_(
Platform.slug.in_(EJS_SUPPORTED_PLATFORMS),
Platform.slug.in_(OTHER_SUPPORTED_PLATFORMS),
Platform.slug.in_(RUFFLE_SUPPORTED_PLATFORMS),
)
if not value:
predicate = not_(predicate)

View File

@@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import InstrumentedAttribute
from decorators.database import begin_session
from endpoints.responses.stats import MetadataCoverageItem, RegionBreakdownItem
from models.assets import Save, Screenshot, State
from models.rom import Rom, RomFile
@@ -101,7 +102,7 @@ class DBStatsHandler(DBBaseHandler):
def get_metadata_coverage_by_platform(
self,
session: Session = None, # type: ignore
) -> dict[int, list[dict]]:
) -> dict[int, list[MetadataCoverageItem]]:
"""Get the count of ROMs matched per metadata source, grouped by platform."""
rows = session.execute(
select(
@@ -115,20 +116,21 @@ class DBStatsHandler(DBBaseHandler):
.group_by(Rom.platform_id)
).all()
result: dict[int, list[dict]] = {}
result: dict[int, list[MetadataCoverageItem]] = {}
for row in rows:
result[row.platform_id] = [
{"source": key, "matched": getattr(row, key)}
MetadataCoverageItem(source=key, matched=getattr(row, key))
for key in _METADATA_SOURCE_COLUMNS
if getattr(row, key) > 0
]
return result
@begin_session
def get_region_breakdown_by_platform(
self,
session: Session = None, # type: ignore
) -> dict[int, list[dict]]:
) -> dict[int, list[RegionBreakdownItem]]:
"""Get the count of ROMs per region, grouped by platform."""
rows = session.execute(
select(Rom.platform_id, Rom.regions).where(Rom.regions.is_not(None))

View File

@@ -95,7 +95,7 @@ class FSResourcesHandler(FSHandler):
else:
# Handle HTTP URLs
# Validate URL to prevent SSRF attacks
validate_url_for_http_request(url_cover, "Cover URL")
validate_url_for_http_request(url_cover, "url_cover")
httpx_client = ctx_httpx_client.get()
try:
@@ -264,7 +264,7 @@ class FSResourcesHandler(FSHandler):
else:
# Handle HTTP URLs
# Validate URL to prevent SSRF attacks
validate_url_for_http_request(url_screenhot, "Screenshot URL")
validate_url_for_http_request(url_screenhot, "url_screenshot")
httpx_client = ctx_httpx_client.get()
try:
@@ -380,7 +380,7 @@ class FSResourcesHandler(FSHandler):
else:
# Handle HTTP URL
# Validate URL to prevent SSRF attacks
validate_url_for_http_request(url_manual, "Manual URL")
validate_url_for_http_request(url_manual, "url_manual")
httpx_client = ctx_httpx_client.get()
try:
@@ -441,7 +441,7 @@ class FSResourcesHandler(FSHandler):
# Retroachievements
async def store_ra_badge(self, url: str, path: str) -> None:
# Validate URL to prevent SSRF attacks
validate_url_for_http_request(url, "Badge URL")
validate_url_for_http_request(url, "url_badge")
httpx_client = ctx_httpx_client.get()
directory, filename = os.path.split(path)
@@ -484,7 +484,7 @@ class FSResourcesHandler(FSHandler):
) -> str:
return os.path.join("roms", str(platform_id), str(rom_id), media_type.value)
async def store_media_file(self, url: str, dest_path: str) -> None:
async def store_media_file(self, url_media: str, dest_path: str) -> None:
httpx_client = ctx_httpx_client.get()
directory, filename = os.path.split(dest_path)
@@ -496,22 +496,24 @@ class FSResourcesHandler(FSHandler):
await self.make_directory(directory)
# Handle file:// URLs for gamelist.xml
if url.startswith("file://"):
if url_media.startswith("file://"):
try:
file_path = AnyioPath(url[7:]) # Remove "file://" prefix
file_path = AnyioPath(url_media[7:]) # Remove "file://" prefix
if await file_path.exists():
await self.copy_file(Path(str(file_path)), dest_path)
except Exception as exc:
log.error(f"Unable to copy media file {url}: {str(exc)}")
log.error(f"Unable to copy media file {url_media}: {str(exc)}")
return None
else:
# Handle HTTP URLs
# Validate URL to prevent SSRF attacks
validate_url_for_http_request(url, "Media URL")
validate_url_for_http_request(url_media, "url_media")
httpx_client = ctx_httpx_client.get()
try:
async with httpx_client.stream("GET", url, timeout=120) as response:
async with httpx_client.stream(
"GET", url_media, timeout=120
) as response:
if response.status_code == status.HTTP_200_OK:
async with await self.write_file_streamed(
path=directory, filename=filename
@@ -519,7 +521,7 @@ class FSResourcesHandler(FSHandler):
async for chunk in response.aiter_raw():
await f.write(chunk)
except httpx.TransportError as exc:
log.error(f"Unable to fetch media file at {url}: {str(exc)}")
log.error(f"Unable to fetch media file at {url_media}: {str(exc)}")
return None
async def remove_media_resources_path(

View File

@@ -34,6 +34,12 @@ export type { BulkOperationResponse } from './models/BulkOperationResponse';
export type { CleanupStats } from './models/CleanupStats';
export type { CleanupTaskMeta } from './models/CleanupTaskMeta';
export type { CleanupTaskStatusResponse } from './models/CleanupTaskStatusResponse';
export type { ClientTokenAdminSchema } from './models/ClientTokenAdminSchema';
export type { ClientTokenCreatePayload } from './models/ClientTokenCreatePayload';
export type { ClientTokenCreateSchema } from './models/ClientTokenCreateSchema';
export type { ClientTokenExchangePayload } from './models/ClientTokenExchangePayload';
export type { ClientTokenPairSchema } from './models/ClientTokenPairSchema';
export type { ClientTokenSchema } from './models/ClientTokenSchema';
export type { CollectionSchema } from './models/CollectionSchema';
export type { ConfigResponse } from './models/ConfigResponse';
export type { ConversionStats } from './models/ConversionStats';
@@ -92,6 +98,7 @@ export type { RomMetadataSchema } from './models/RomMetadataSchema';
export type { RomMobyMetadata } from './models/RomMobyMetadata';
export type { RomRAMetadata } from './models/RomRAMetadata';
export type { RomSSMetadata } from './models/RomSSMetadata';
export type { RomUserData } from './models/RomUserData';
export type { RomUserSchema } from './models/RomUserSchema';
export type { RomUserStatus } from './models/RomUserStatus';
export type { RomUserUpdatePayload } from './models/RomUserUpdatePayload';

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ClientTokenAdminSchema = {
id: number;
name: string;
scopes: Array<string>;
expires_at: (string | null);
last_used_at: (string | null);
created_at: string;
user_id: number;
username: string;
};

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ClientTokenCreatePayload = {
name: string;
scopes: Array<string>;
expires_in?: (string | null);
};

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ClientTokenCreateSchema = {
id: number;
name: string;
scopes: Array<string>;
expires_at: (string | null);
last_used_at: (string | null);
created_at: string;
user_id: number;
raw_token: string;
};

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ClientTokenExchangePayload = {
code: string;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ClientTokenPairSchema = {
code: string;
expires_in: number;
};

View File

@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ClientTokenSchema = {
id: number;
name: string;
scopes: Array<string>;
expires_at: (string | null);
last_used_at: (string | null);
created_at: string;
user_id: number;
};

View File

@@ -9,6 +9,7 @@ export type RAUserGameProgression = {
num_awarded: (number | null);
num_awarded_hardcore: (number | null);
most_recent_awarded_date?: (string | null);
highest_award_kind?: (string | null);
earned_achievements: Array<EarnedAchievement>;
};

View File

@@ -0,0 +1,40 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RomUserStatus } from './RomUserStatus';
export type RomUserData = {
/**
* Whether this rom is the main sibling.
*/
is_main_sibling?: (boolean | null);
/**
* Whether this rom is in the backlog.
*/
backlogged?: (boolean | null);
/**
* Whether this rom is currently being played.
*/
now_playing?: (boolean | null);
/**
* Whether this rom is hidden.
*/
hidden?: (boolean | null);
/**
* User rating for this rom (0-10).
*/
rating?: (number | null);
/**
* User difficulty rating for this rom (0-10).
*/
difficulty?: (number | null);
/**
* User completion percentage for this rom (0-100).
*/
completion?: (number | null);
/**
* User play status for this rom.
*/
status?: (RomUserStatus | null);
};

View File

@@ -2,9 +2,19 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RomUserData } from './RomUserData';
export type RomUserUpdatePayload = {
data?: Record<string, any>;
/**
* Partial rom user data to update. Only provided fields will be updated.
*/
data?: RomUserData;
/**
* Set last played timestamp to now.
*/
update_last_played?: boolean;
/**
* Clear the last played timestamp.
*/
remove_last_played?: boolean;
};

View File

@@ -4,9 +4,9 @@
/* eslint-disable */
export type TokenResponse = {
access_token: string;
refresh_token?: string;
token_type: string;
expires: number;
refresh_token?: string;
refresh_expires?: number;
};

View File

@@ -10,10 +10,6 @@ async function getTaskById(taskId: string) {
return api.get<TaskStatusResponse>(`/tasks/${taskId}`);
}
async function runAllTasks() {
return api.post<TaskExecutionResponse[]>("/tasks/run");
}
async function runTask(taskName: string, body?: Record<string, unknown>) {
return api.post<TaskExecutionResponse>(`/tasks/run/${taskName}`, body);
}
@@ -25,7 +21,6 @@ async function getTaskStatus() {
export default {
getTasks,
getTaskById,
runAllTasks,
runTask,
getTaskStatus,
};