Compare commits

...
2 Commits
Author SHA1 Message Date
hjlarry e007dbaf0e fix: use contracts 2026-06-17 09:52:04 +08:00
hjlarry 9323b9f49d chore: add client country api 2026-06-16 16:04:09 +08:00
7 changed files with 172 additions and 0 deletions
+2
View File
@@ -34,6 +34,7 @@ for module_name in RESOURCE_MODULES:
# Import other controllers
from . import (
apikey,
client_metadata,
extension,
feature,
human_input_form,
@@ -167,6 +168,7 @@ __all__ = [
"banner",
"billing",
"bp",
"client_metadata",
"completion",
"compliance",
"console_ns",
@@ -0,0 +1,52 @@
"""Public client metadata endpoint backed by Cloudflare request metadata."""
import re
from flask import request
from flask_restx import Resource
from pydantic import Field
from controllers.common.schema import JsonResponseWithStatus, register_response_schema_models
from fields.base import ResponseModel
from libs.helper import dump_response
from . import console_ns
COUNTRY_CODE_PATTERN = re.compile(r"^[A-Z0-9]{2}$")
class ClientGeoMetadataResponse(ResponseModel):
country_code: str | None = Field(
default=None,
description="Two-character Cloudflare country code for the current request, or null when unavailable.",
examples=["CN"],
)
class ClientMetadataResponse(ResponseModel):
geo: ClientGeoMetadataResponse = Field(description="Geographic metadata inferred for the current request.")
register_response_schema_models(console_ns, ClientMetadataResponse)
def _extract_cloudflare_country_code() -> str | None:
country_code = request.headers.get("CF-IPCountry", "").strip().upper()
if not COUNTRY_CODE_PATTERN.fullmatch(country_code):
return None
return country_code
@console_ns.route("/client-metadata")
class ClientMetadataApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ClientMetadataResponse.__name__])
def get(self) -> JsonResponseWithStatus:
"""Return client metadata inferred from edge request headers.
NOTE: This endpoint is unauthenticated by design. It exposes only low-sensitivity
metadata for the current request and performs no tenant, account, or setup checks.
"""
return dump_response(
ClientMetadataResponse,
{"geo": {"country_code": _extract_cloudflare_country_code()}},
), 200
@@ -0,0 +1,36 @@
import pytest
from flask import Flask
from controllers.console import bp as console_bp
@pytest.fixture
def app() -> Flask:
app = Flask(__name__)
app.config["TESTING"] = True
app.register_blueprint(console_bp)
return app
def test_console_client_metadata_returns_cloudflare_geo(app: Flask):
client = app.test_client()
response = client.get("/console/api/client-metadata", headers={"CF-IPCountry": "cn"})
assert response.status_code == 200
assert response.get_json() == {"geo": {"country_code": "CN"}}
def test_console_client_metadata_returns_null_geo_without_cloudflare_header(app: Flask):
client = app.test_client()
response = client.get("/console/api/client-metadata")
assert response.status_code == 200
assert response.get_json() == {"geo": {"country_code": None}}
def test_console_client_metadata_ignores_invalid_cloudflare_geo(app: Flask):
client = app.test_client()
response = client.get("/console/api/client-metadata", headers={"CF-IPCountry": "China"})
assert response.status_code == 200
assert response.get_json() == {"geo": {"country_code": None}}
@@ -0,0 +1,32 @@
// This file is auto-generated by @hey-api/openapi-ts
import { oc } from '@orpc/contract'
import { zGetClientMetadataResponse } from './zod.gen'
/**
* Return client metadata inferred from edge request headers
*
* NOTE: This endpoint is unauthenticated by design. It exposes only low-sensitivity
* metadata for the current request and performs no tenant, account, or setup checks.
*/
export const get = oc
.route({
description:
'NOTE: This endpoint is unauthenticated by design. It exposes only low-sensitivity\nmetadata for the current request and performs no tenant, account, or setup checks.',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getClientMetadata',
path: '/client-metadata',
summary: 'Return client metadata inferred from edge request headers',
tags: ['console'],
})
.output(zGetClientMetadataResponse)
export const clientMetadata = {
get,
}
export const contract = {
clientMetadata,
}
@@ -0,0 +1,26 @@
// This file is auto-generated by @hey-api/openapi-ts
export type ClientOptions = {
baseUrl: `${string}://${string}/console/api` | (string & {})
}
export type ClientMetadataResponse = {
geo: ClientGeoMetadataResponse
}
export type ClientGeoMetadataResponse = {
country_code?: string | null
}
export type GetClientMetadataData = {
body?: never
path?: never
query?: never
url: '/client-metadata'
}
export type GetClientMetadataResponses = {
200: ClientMetadataResponse
}
export type GetClientMetadataResponse = GetClientMetadataResponses[keyof GetClientMetadataResponses]
@@ -0,0 +1,22 @@
// This file is auto-generated by @hey-api/openapi-ts
import * as z from 'zod'
/**
* ClientGeoMetadataResponse
*/
export const zClientGeoMetadataResponse = z.object({
country_code: z.string().nullish(),
})
/**
* ClientMetadataResponse
*/
export const zClientMetadataResponse = z.object({
geo: zClientGeoMetadataResponse,
})
/**
* Success
*/
export const zGetClientMetadataResponse = zClientMetadataResponse
@@ -11,6 +11,7 @@ import { app } from './app/orpc.gen'
import { apps } from './apps/orpc.gen'
import { auth } from './auth/orpc.gen'
import { billing } from './billing/orpc.gen'
import { clientMetadata } from './client-metadata/orpc.gen'
import { codeBasedExtension } from './code-based-extension/orpc.gen'
import { compliance } from './compliance/orpc.gen'
import { dataSource } from './data-source/orpc.gen'
@@ -62,6 +63,7 @@ export const contract = {
apps,
auth,
billing,
clientMetadata,
codeBasedExtension,
compliance,
dataSource,