Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api/.api-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.8.7
v0.8.8
4 changes: 4 additions & 0 deletions docs/api/http-api/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,13 @@ the device record and creates a new identity session.

No authentication is required to call this endpoint (it IS the authentication).

The _geo parameter triggers GeoIP lookup for this endpoint. Geo data is
available for future use via _geo.timezone, _geo.country, _geo.city.

Args:
session_request: Session start request with game_id and fingerprint
identity_service: IdentityService dependency (handles device and identity creation)
_geo: GeoIP information extracted from client IP address (available for future use)

Returns:
StartSessionResponse with identity info and access tokens
Expand Down
4 changes: 2 additions & 2 deletions docs/api/http-api/index.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: LEADR Open-Source Edition - Admin & Client API v0.8.6
title: LEADR Open-Source Edition - Admin & Client API v0.8.7
---

# LEADR Open-Source Edition - Admin & Client API v0.8.6
# LEADR Open-Source Edition - Admin & Client API v0.8.7

> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu.

Expand Down
2 changes: 1 addition & 1 deletion docs/api/http-api/scores.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ are automatically derived from the authenticated session.

Args:
score_request: Score creation details including board_id, player_name, and value.
request: FastAPI request object for accessing geo data.
geo: GeoIP information extracted from client IP address.
service: Injected score service dependency.
board_service: Injected board service for board lookup.
background_tasks: FastAPI background tasks for async metadata updates.
Expand Down
6 changes: 3 additions & 3 deletions docs/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "LEADR Open-Source Edition - Admin & Client API",
"description": "LEADR Open-Source Edition is the free cross-platform leaderboard backend for game devs",
"version": "0.8.6"
"version": "0.8.7"
},
"paths": {
"/v1/accounts": {
Expand Down Expand Up @@ -7321,7 +7321,7 @@
"Scores"
],
"summary": "Create Score Client",
"description": "Create a new score (Client API).\n\nCreates a new score submission for a board. All IDs (account_id, game_id, identity_id)\nare automatically derived from the authenticated session.\n\nArgs:\n score_request: Score creation details including board_id, player_name, and value.\n request: FastAPI request object for accessing geo data.\n service: Injected score service dependency.\n board_service: Injected board service for board lookup.\n background_tasks: FastAPI background tasks for async metadata updates.\n auth: Client authentication context with device and identity info.\n pre_create_hook: Hook called before score creation (for quota checks).\n post_create_hook: Hook called after successful score creation.\n\nReturns:\n ScoreClientResponse with the created score (excludes device_id).\n\nRaises:\n 404: Board not found.\n 400: Validation failed (board doesn't belong to account, or game doesn't\n match board's game).\n 403: Score rejected by anti-cheat (rate limit exceeded).",
"description": "Create a new score (Client API).\n\nCreates a new score submission for a board. All IDs (account_id, game_id, identity_id)\nare automatically derived from the authenticated session.\n\nArgs:\n score_request: Score creation details including board_id, player_name, and value.\n geo: GeoIP information extracted from client IP address.\n service: Injected score service dependency.\n board_service: Injected board service for board lookup.\n background_tasks: FastAPI background tasks for async metadata updates.\n auth: Client authentication context with device and identity info.\n pre_create_hook: Hook called before score creation (for quota checks).\n post_create_hook: Hook called after successful score creation.\n\nReturns:\n ScoreClientResponse with the created score (excludes device_id).\n\nRaises:\n 404: Board not found.\n 400: Validation failed (board doesn't belong to account, or game doesn't\n match board's game).\n 403: Score rejected by anti-cheat (rate limit exceeded).",
"operationId": "create_score_client_v1_client_scores_post",
"parameters": [
{
Expand Down Expand Up @@ -7985,7 +7985,7 @@
"Authentication"
],
"summary": "Start Session",
"description": "Start a new identity session for a game client.\n\nThis endpoint authenticates game clients and provides JWT access tokens.\nIt is idempotent - calling multiple times for the same fingerprint updates\nthe device record and creates a new identity session.\n\nNo authentication is required to call this endpoint (it IS the authentication).\n\nArgs:\n session_request: Session start request with game_id and fingerprint\n identity_service: IdentityService dependency (handles device and identity creation)\n\nReturns:\n StartSessionResponse with identity info and access tokens\n\nRaises:\n 404: Game not found\n 422: Invalid request (missing required fields, invalid UUID format)",
"description": "Start a new identity session for a game client.\n\nThis endpoint authenticates game clients and provides JWT access tokens.\nIt is idempotent - calling multiple times for the same fingerprint updates\nthe device record and creates a new identity session.\n\nNo authentication is required to call this endpoint (it IS the authentication).\n\nThe _geo parameter triggers GeoIP lookup for this endpoint. Geo data is\navailable for future use via _geo.timezone, _geo.country, _geo.city.\n\nArgs:\n session_request: Session start request with game_id and fingerprint\n identity_service: IdentityService dependency (handles device and identity creation)\n _geo: GeoIP information extracted from client IP address (available for future use)\n\nReturns:\n StartSessionResponse with identity info and access tokens\n\nRaises:\n 404: Game not found\n 422: Invalid request (missing required fields, invalid UUID format)",
"operationId": "start_session_v1_client_sessions_post",
"requestBody": {
"content": {
Expand Down
6 changes: 3 additions & 3 deletions docs/api/reference/accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ slug: Mapped[str] = mapped_column(String, nullable=False, unique=True, index=Tru
####### `leadr.accounts.adapters.orm.AccountORM.status`

```python
status: Mapped[AccountStatusEnum] = mapped_column(Enum(AccountStatusEnum, name='account_status', native_enum=True, values_callable=(lambda x: [(e.value) for e in x])), nullable=False, default=(AccountStatusEnum.ACTIVE), server_default='active')
status: Mapped[AccountStatusEnum] = mapped_column(Enum(AccountStatusEnum, name='account_status', native_enum=True, values_callable=(lambda x: [(e.value) for e in x])), nullable=False, default=(AccountStatusEnum.ACTIVE), server_default='active', index=True)
```

####### `leadr.accounts.adapters.orm.AccountORM.updated_at`
Expand Down Expand Up @@ -182,7 +182,7 @@ id: Mapped[uuid_pk]
####### `leadr.accounts.adapters.orm.UserORM.is_owner`

```python
is_owner: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='false')
is_owner: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='false', index=True)
```

####### `leadr.accounts.adapters.orm.UserORM.status`
Expand All @@ -194,7 +194,7 @@ status: Mapped[UserStatusEnum] = mapped_column(Enum(UserStatusEnum, name='user_s
####### `leadr.accounts.adapters.orm.UserORM.super_admin`

```python
super_admin: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='false')
super_admin: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='false', index=True)
```

####### `leadr.accounts.adapters.orm.UserORM.updated_at`
Expand Down
112 changes: 110 additions & 2 deletions docs/api/reference/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,7 @@ No authentication is required (the refresh token itself is the credential).
###### `leadr.auth.api.client_routes.start_session`

```python
start_session(session_request, identity_service)
start_session(session_request, identity_service, _geo)
```

Start a new identity session for a game client.
Expand All @@ -1231,10 +1231,14 @@ the device record and creates a new identity session.

No authentication is required to call this endpoint (it IS the authentication).

The \_geo parameter triggers GeoIP lookup for this endpoint. Geo data is
available for future use via \_geo.timezone, \_geo.country, \_geo.city.

**Parameters:**

- **session_request** (<code>[StartSessionRequest](#leadr.auth.api.client_schemas.StartSessionRequest)</code>) – Session start request with game_id and fingerprint
- **identity_service** (<code>[IdentityServiceDep](./auth.md#leadr.auth.services.dependencies.IdentityServiceDep)</code>) – IdentityService dependency (handles device and identity creation)
- **\_geo** (<code>[GeoInfoDep](./common.md#leadr.common.dependencies.GeoInfoDep)</code>) – GeoIP information extracted from client IP address (available for future use)

**Returns:**

Expand Down Expand Up @@ -4097,10 +4101,13 @@ and repository layer.
- [**list_all**](#leadr.auth.services.api_key_service.APIKeyService.list_all) – List all non-deleted entities.
- [**list_api_keys**](#leadr.auth.services.api_key_service.APIKeyService.list_api_keys) – List API keys for an account with optional filters and pagination.
- [**record_usage**](#leadr.auth.services.api_key_service.APIKeyService.record_usage) – Record that an API key was used at a specific time.
- [**record_usage_async**](#leadr.auth.services.api_key_service.APIKeyService.record_usage_async) – Record API key usage asynchronously (for background tasks).
- [**revoke_api_key**](#leadr.auth.services.api_key_service.APIKeyService.revoke_api_key) – Revoke an API key, preventing further use.
- [**should_update_usage**](#leadr.auth.services.api_key_service.APIKeyService.should_update_usage) – Check if last_used_at needs updating based on configurable threshold.
- [**soft_delete**](#leadr.auth.services.api_key_service.APIKeyService.soft_delete) – Soft-delete an entity and return it before deletion.
- [**update_api_key_status**](#leadr.auth.services.api_key_service.APIKeyService.update_api_key_status) – Update the status of an API key.
- [**validate_api_key**](#leadr.auth.services.api_key_service.APIKeyService.validate_api_key) – Validate an API key and return the domain entity if valid.
- [**validate_api_key_with_user**](#leadr.auth.services.api_key_service.APIKeyService.validate_api_key_with_user) – Validate an API key and return both the key and associated user.

**Attributes:**

Expand Down Expand Up @@ -4343,6 +4350,21 @@ also be called explicitly if needed.

- <code>[EntityNotFoundError](./common.md#leadr.common.domain.exceptions.EntityNotFoundError)</code> – If the key doesn't exist.

####### `leadr.auth.services.api_key_service.APIKeyService.record_usage_async`

```python
record_usage_async(key_id)
```

Record API key usage asynchronously (for background tasks).

This method is designed to be called as a background task. It silently
handles missing keys to avoid errors in background processing.

**Parameters:**

- **key_id** (<code>[APIKeyID](./common.md#leadr.common.domain.ids.APIKeyID)</code>) – The ID of the API key that was used.

####### `leadr.auth.services.api_key_service.APIKeyService.repository`

```python
Expand All @@ -4369,6 +4391,27 @@ Revoke an API key, preventing further use.

- <code>[EntityNotFoundError](./common.md#leadr.common.domain.exceptions.EntityNotFoundError)</code> – If the key doesn't exist.

####### `leadr.auth.services.api_key_service.APIKeyService.should_update_usage`

```python
should_update_usage(api_key)
```

Check if last_used_at needs updating based on configurable threshold.

Returns True if:

- last_used_at is None (never used before), or
- last_used_at is older than API_KEY_USAGE_UPDATE_THRESHOLD_SECONDS

**Parameters:**

- **api_key** (<code>[APIKey](#leadr.auth.domain.api_key.APIKey)</code>) – The API key to check.

**Returns:**

- <code>[bool](#bool)</code> – True if usage should be updated, False otherwise.

####### `leadr.auth.services.api_key_service.APIKeyService.soft_delete`

```python
Expand Down Expand Up @@ -4427,7 +4470,10 @@ Performs the following checks:
1. Verifies the hash matches
1. Checks if key is active (not revoked)
1. Checks if key is not expired
1. Records usage timestamp if valid

Note: This method does NOT update last_used_at. The caller should check
should_update_usage() and schedule record_usage_async() via background task
if needed.

**Parameters:**

Expand All @@ -4448,6 +4494,48 @@ Performs the following checks:

</details>

####### `leadr.auth.services.api_key_service.APIKeyService.validate_api_key_with_user`

```python
validate_api_key_with_user(plain_key)
```

Validate an API key and return both the key and associated user.

This method performs a single JOIN query to retrieve both the API key
and user in one database round-trip, reducing auth latency.

Performs the following checks:

1. Extracts prefix and looks up key + user in database via JOIN
1. Verifies the hash matches
1. Checks if key is active (not revoked)
1. Checks if key is not expired

Note: This method does NOT update last_used_at. The caller should check
should_update_usage() and schedule record_usage_async() via background task
if needed.

**Parameters:**

- **plain_key** (<code>[str](#str)</code>) – The plain API key string to validate.

**Returns:**

- <code>[tuple](#tuple)\[[APIKey](#leadr.auth.domain.api_key.APIKey), [User](./accounts.md#leadr.accounts.domain.user.User)\] | None</code> – A tuple of (APIKey, User) if valid, None otherwise.

<details class="example" open markdown="1">
<summary>Example</summary>

> > > result = await service.validate_api_key_with_user("ldr_abc123...")
> > > if result:
> > > ... api_key, user = result
> > > ... print(f"Valid key for user {user.email}")
> > > ... else:
> > > ... print("Invalid or expired key")

</details>

##### `leadr.auth.services.dependencies`

Auth service dependency injection factories.
Expand Down Expand Up @@ -5715,6 +5803,7 @@ API Key repository for managing API key persistence.
- [**filter**](./auth.md#leadr.auth.services.repositories.APIKeyRepository.filter) – Filter API keys by account and optional criteria with pagination.
- [**get_by_id**](#leadr.auth.services.repositories.APIKeyRepository.get_by_id) – Get an entity by its ID.
- [**get_by_prefix**](#leadr.auth.services.repositories.APIKeyRepository.get_by_prefix) – Get API key by prefix, returns None if not found or soft-deleted.
- [**get_by_prefix_with_user**](#leadr.auth.services.repositories.APIKeyRepository.get_by_prefix_with_user) – Get API key and associated user in a single query.
- [**update**](./auth.md#leadr.auth.services.repositories.APIKeyRepository.update) – Update an existing entity in the database.

**Attributes:**
Expand Down Expand Up @@ -5827,6 +5916,25 @@ get_by_prefix(key_prefix)

Get API key by prefix, returns None if not found or soft-deleted.

####### `leadr.auth.services.repositories.APIKeyRepository.get_by_prefix_with_user`

```python
get_by_prefix_with_user(key_prefix)
```

Get API key and associated user in a single query.

This method performs a JOIN between api_keys and users tables to retrieve
both entities in one database round-trip, reducing auth latency.

**Parameters:**

- **key_prefix** (<code>[str](#str)</code>) – The API key prefix to search for.

**Returns:**

- <code>[tuple](#tuple)\[[APIKey](#leadr.auth.domain.api_key.APIKey), [User](./accounts.md#leadr.accounts.domain.user.User)\] | None</code> – A tuple of (APIKey, User) if found and neither soft-deleted, None otherwise.

####### `leadr.auth.services.repositories.APIKeyRepository.session`

```python
Expand Down
10 changes: 5 additions & 5 deletions docs/api/reference/boards.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ created_at: Mapped[timestamp]
####### `leadr.boards.adapters.orm.BoardORM.created_from_template_id`

```python
created_from_template_id: Mapped[UUID | None] = mapped_column(nullable=True, default=None)
created_from_template_id: Mapped[UUID | None] = mapped_column(nullable=True, default=None, index=True)
```

####### `leadr.boards.adapters.orm.BoardORM.deleted_at`
Expand Down Expand Up @@ -144,13 +144,13 @@ id: Mapped[uuid_pk]
####### `leadr.boards.adapters.orm.BoardORM.is_active`

```python
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, index=True)
```

####### `leadr.boards.adapters.orm.BoardORM.is_published`

```python
is_published: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=(sa.text('true')))
is_published: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=(sa.text('true')), index=True)
```

####### `leadr.boards.adapters.orm.BoardORM.keep_strategy`
Expand Down Expand Up @@ -607,7 +607,7 @@ id: Mapped[uuid_pk]
####### `leadr.boards.adapters.orm.BoardTemplateORM.is_active`

```python
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, index=True)
```

####### `leadr.boards.adapters.orm.BoardTemplateORM.is_published`
Expand Down Expand Up @@ -637,7 +637,7 @@ name_template: Mapped[str | None] = mapped_column(String, nullable=True, default
####### `leadr.boards.adapters.orm.BoardTemplateORM.next_run_at`

```python
next_run_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
next_run_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
```

####### `leadr.boards.adapters.orm.BoardTemplateORM.repeat_interval`
Expand Down
52 changes: 52 additions & 0 deletions docs/api/reference/common.md
Original file line number Diff line number Diff line change
Expand Up @@ -858,16 +858,68 @@ handles cleanup and rollback on exceptions.

Shared FastAPI dependencies for the application.

**Functions:**

- [**get_geo_info**](#leadr.common.dependencies.get_geo_info) – FastAPI dependency to get GeoIP info for the request.

**Attributes:**

- [**DatabaseSession**](./common.md#leadr.common.dependencies.DatabaseSession) –
- [**GeoInfoDep**](./common.md#leadr.common.dependencies.GeoInfoDep) –
- [**logger**](./common.md#leadr.common.dependencies.logger) –

##### `leadr.common.dependencies.DatabaseSession`

```python
DatabaseSession = Annotated[AsyncSession, Depends(get_db)]
```

##### `leadr.common.dependencies.GeoInfoDep`

```python
GeoInfoDep = Annotated[GeoInfo, Depends(get_geo_info)]
```

##### `leadr.common.dependencies.get_geo_info`

```python
get_geo_info(request)
```

FastAPI dependency to get GeoIP info for the request.

This dependency performs GeoIP lookup for the client's IP address. It's
designed to be used on specific endpoints (like score submissions) rather
than globally as middleware.

The dependency gracefully handles failures - if GeoIP lookup fails for any
reason, it returns a GeoInfo with all None fields.

**Parameters:**

- **request** (<code>[Request](#fastapi.Request)</code>) – The incoming FastAPI request.

**Returns:**

- <code>[GeoInfo](./common.md#leadr.common.geoip.GeoInfo)</code> – GeoInfo with timezone, country, and city (all may be None).

<details class="example" open markdown="1">
<summary>Example</summary>

@router.post("/scores")
async def submit_score(geo: GeoInfoDep):
timezone = geo.timezone
country = geo.country
city = geo.city

</details>

##### `leadr.common.dependencies.logger`

```python
logger = logging.getLogger(__name__)
```

#### `leadr.common.domain`

**Modules:**
Expand Down
Loading