Compare commits
2
Commits
main
...
feat/country
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e007dbaf0e | ||
|
|
9323b9f49d |
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user