diff --git a/backend/endpoints/responses/oauth.py b/backend/endpoints/responses/oauth.py index 1d403ff65..114837a49 100644 --- a/backend/endpoints/responses/oauth.py +++ b/backend/endpoints/responses/oauth.py @@ -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] diff --git a/backend/endpoints/tasks.py b/backend/endpoints/tasks.py index 643c563e6..5b2c50789 100644 --- a/backend/endpoints/tasks.py +++ b/backend/endpoints/tasks.py @@ -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) diff --git a/backend/endpoints/user.py b/backend/endpoints/user.py index d0549e2ab..6bee3547e 100644 --- a/backend/endpoints/user.py +++ b/backend/endpoints/user.py @@ -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, diff --git a/backend/handler/auth/base_handler.py b/backend/handler/auth/base_handler.py index 8b116b2d3..94c60bcf0 100644 --- a/backend/handler/auth/base_handler.py +++ b/backend/handler/auth/base_handler.py @@ -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 diff --git a/backend/handler/database/client_tokens_handler.py b/backend/handler/database/client_tokens_handler.py index 7bd27a5d8..6a3aec69e 100644 --- a/backend/handler/database/client_tokens_handler.py +++ b/backend/handler/database/client_tokens_handler.py @@ -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) diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 3fadcb285..da1875937 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -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) diff --git a/backend/handler/database/stats_handler.py b/backend/handler/database/stats_handler.py index 46a377ea9..6c065e6f6 100644 --- a/backend/handler/database/stats_handler.py +++ b/backend/handler/database/stats_handler.py @@ -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)) diff --git a/backend/handler/filesystem/resources_handler.py b/backend/handler/filesystem/resources_handler.py index 8bc4baf7c..2b2b334f4 100644 --- a/backend/handler/filesystem/resources_handler.py +++ b/backend/handler/filesystem/resources_handler.py @@ -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( diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index 5bda51184..60e4521d1 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -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'; diff --git a/frontend/src/__generated__/models/ClientTokenAdminSchema.ts b/frontend/src/__generated__/models/ClientTokenAdminSchema.ts new file mode 100644 index 000000000..3439aea49 --- /dev/null +++ b/frontend/src/__generated__/models/ClientTokenAdminSchema.ts @@ -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; + expires_at: (string | null); + last_used_at: (string | null); + created_at: string; + user_id: number; + username: string; +}; + diff --git a/frontend/src/__generated__/models/ClientTokenCreatePayload.ts b/frontend/src/__generated__/models/ClientTokenCreatePayload.ts new file mode 100644 index 000000000..54d32b1c1 --- /dev/null +++ b/frontend/src/__generated__/models/ClientTokenCreatePayload.ts @@ -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; + expires_in?: (string | null); +}; + diff --git a/frontend/src/__generated__/models/ClientTokenCreateSchema.ts b/frontend/src/__generated__/models/ClientTokenCreateSchema.ts new file mode 100644 index 000000000..8f2cc7a83 --- /dev/null +++ b/frontend/src/__generated__/models/ClientTokenCreateSchema.ts @@ -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; + expires_at: (string | null); + last_used_at: (string | null); + created_at: string; + user_id: number; + raw_token: string; +}; + diff --git a/frontend/src/__generated__/models/ClientTokenExchangePayload.ts b/frontend/src/__generated__/models/ClientTokenExchangePayload.ts new file mode 100644 index 000000000..f250039ff --- /dev/null +++ b/frontend/src/__generated__/models/ClientTokenExchangePayload.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ClientTokenExchangePayload = { + code: string; +}; + diff --git a/frontend/src/__generated__/models/ClientTokenPairSchema.ts b/frontend/src/__generated__/models/ClientTokenPairSchema.ts new file mode 100644 index 000000000..fbf7a50be --- /dev/null +++ b/frontend/src/__generated__/models/ClientTokenPairSchema.ts @@ -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; +}; + diff --git a/frontend/src/__generated__/models/ClientTokenSchema.ts b/frontend/src/__generated__/models/ClientTokenSchema.ts new file mode 100644 index 000000000..52feebdec --- /dev/null +++ b/frontend/src/__generated__/models/ClientTokenSchema.ts @@ -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; + expires_at: (string | null); + last_used_at: (string | null); + created_at: string; + user_id: number; +}; + diff --git a/frontend/src/__generated__/models/RAUserGameProgression.ts b/frontend/src/__generated__/models/RAUserGameProgression.ts index fbdb8eafd..4e6226528 100644 --- a/frontend/src/__generated__/models/RAUserGameProgression.ts +++ b/frontend/src/__generated__/models/RAUserGameProgression.ts @@ -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; }; diff --git a/frontend/src/__generated__/models/RomUserData.ts b/frontend/src/__generated__/models/RomUserData.ts new file mode 100644 index 000000000..c279aad7d --- /dev/null +++ b/frontend/src/__generated__/models/RomUserData.ts @@ -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); +}; + diff --git a/frontend/src/__generated__/models/RomUserUpdatePayload.ts b/frontend/src/__generated__/models/RomUserUpdatePayload.ts index d636ce860..6536a26d6 100644 --- a/frontend/src/__generated__/models/RomUserUpdatePayload.ts +++ b/frontend/src/__generated__/models/RomUserUpdatePayload.ts @@ -2,9 +2,19 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { RomUserData } from './RomUserData'; export type RomUserUpdatePayload = { - data?: Record; + /** + * 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; }; diff --git a/frontend/src/__generated__/models/TokenResponse.ts b/frontend/src/__generated__/models/TokenResponse.ts index 854006909..b40560f57 100644 --- a/frontend/src/__generated__/models/TokenResponse.ts +++ b/frontend/src/__generated__/models/TokenResponse.ts @@ -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; }; diff --git a/frontend/src/services/api/task.ts b/frontend/src/services/api/task.ts index 02e3bee80..494a0080f 100644 --- a/frontend/src/services/api/task.ts +++ b/frontend/src/services/api/task.ts @@ -10,10 +10,6 @@ async function getTaskById(taskId: string) { return api.get(`/tasks/${taskId}`); } -async function runAllTasks() { - return api.post("/tasks/run"); -} - async function runTask(taskName: string, body?: Record) { return api.post(`/tasks/run/${taskName}`, body); } @@ -25,7 +21,6 @@ async function getTaskStatus() { export default { getTasks, getTaskById, - runAllTasks, runTask, getTaskStatus, };