search a ads
This commit is contained in:
+157
-42
@@ -6,16 +6,17 @@ supplied per request as an X- header and used only to talk to the upstream API
|
||||
``Header`` parameters makes them appear per-operation in Swagger, including the
|
||||
"Try it out" form.
|
||||
|
||||
Google Analytics (chosen model: token has precedence over service account):
|
||||
* ``X-GA-Access-Token`` - a ready OAuth2 access token; used directly as Bearer.
|
||||
* ``X-GA-Credentials`` - base64-encoded service-account JSON key; the proxy
|
||||
mints a short-lived access token from it.
|
||||
* ``X-GA-Quota-Project`` - optional billing/quota project id.
|
||||
At least one of token / credentials must be present.
|
||||
All three Google services share the same OAuth mechanism (token wins over
|
||||
service account); they differ only in the OAuth *scope* and the header prefix:
|
||||
|
||||
Sklik:
|
||||
* ``X-Sklik-Token`` - the Sklik API token from account settings. The proxy
|
||||
calls ``client.loginByToken`` to obtain a session.
|
||||
* Google Analytics -> ``X-GA-*`` (scope analytics.readonly)
|
||||
* Search Console -> ``X-GSC-*`` (scope webmasters.readonly)
|
||||
* Google Ads -> ``X-GAds-*`` (scope adwords) + developer token
|
||||
|
||||
Each provides ``*-Access-Token`` (ready Bearer token, takes precedence) and
|
||||
``*-Credentials`` (base64 service-account JSON; the proxy mints a token).
|
||||
|
||||
Sklik uses ``X-Sklik-Token`` (the proxy calls client.loginByToken).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -26,17 +27,72 @@ from dataclasses import dataclass
|
||||
|
||||
from fastapi import Header
|
||||
|
||||
from . import config
|
||||
from .errors import MissingCredentialsError
|
||||
|
||||
|
||||
# --- Google Analytics ---------------------------------------------------------
|
||||
# --- Google (shared) ----------------------------------------------------------
|
||||
@dataclass
|
||||
class GaCredentials:
|
||||
class GoogleCredentials:
|
||||
access_token: str | None
|
||||
service_account_info: dict | None
|
||||
quota_project: str | None
|
||||
scope: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class GoogleAdsCredentials:
|
||||
google: GoogleCredentials
|
||||
developer_token: str
|
||||
login_customer_id: str | None
|
||||
|
||||
|
||||
def _build_google_credentials(
|
||||
access_token: str | None,
|
||||
raw_credentials: str | None,
|
||||
quota_project: str | None,
|
||||
scope: str,
|
||||
*,
|
||||
token_header: str,
|
||||
creds_header: str,
|
||||
) -> GoogleCredentials:
|
||||
"""Parse a Google access token / base64 service-account JSON from headers."""
|
||||
token = (access_token or "").strip() or None
|
||||
|
||||
service_account_info: dict | None = None
|
||||
raw = (raw_credentials or "").strip()
|
||||
if raw:
|
||||
try:
|
||||
decoded = base64.b64decode(raw, validate=True)
|
||||
except (binascii.Error, ValueError) as exc:
|
||||
raise MissingCredentialsError(
|
||||
f"{creds_header} is not valid base64."
|
||||
) from exc
|
||||
try:
|
||||
service_account_info = json.loads(decoded)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
raise MissingCredentialsError(
|
||||
f"{creds_header} does not decode to valid JSON."
|
||||
) from exc
|
||||
if not isinstance(service_account_info, dict):
|
||||
raise MissingCredentialsError(
|
||||
f"{creds_header} JSON must be a service-account object."
|
||||
)
|
||||
|
||||
if not token and service_account_info is None:
|
||||
raise MissingCredentialsError(
|
||||
f"Provide either {token_header} or {creds_header}."
|
||||
)
|
||||
|
||||
return GoogleCredentials(
|
||||
access_token=token,
|
||||
service_account_info=service_account_info,
|
||||
quota_project=(quota_project or "").strip() or None,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
|
||||
# --- Google Analytics ---------------------------------------------------------
|
||||
def get_ga_credentials(
|
||||
x_ga_access_token: str | None = Header(
|
||||
default=None,
|
||||
@@ -54,42 +110,101 @@ def get_ga_credentials(
|
||||
x_ga_quota_project: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GA-Quota-Project",
|
||||
description="Optional Google Cloud project id used for quota/billing "
|
||||
description="Optional Google Cloud project id for quota/billing "
|
||||
"(sets the x-goog-user-project header upstream).",
|
||||
),
|
||||
) -> GaCredentials:
|
||||
"""Resolve GA credentials from headers. Token wins over service account."""
|
||||
access_token = (x_ga_access_token or "").strip() or None
|
||||
) -> GoogleCredentials:
|
||||
return _build_google_credentials(
|
||||
x_ga_access_token,
|
||||
x_ga_credentials,
|
||||
x_ga_quota_project,
|
||||
config.GA_SCOPE,
|
||||
token_header="X-GA-Access-Token",
|
||||
creds_header="X-GA-Credentials",
|
||||
)
|
||||
|
||||
service_account_info: dict | None = None
|
||||
raw = (x_ga_credentials or "").strip()
|
||||
if raw:
|
||||
try:
|
||||
decoded = base64.b64decode(raw, validate=True)
|
||||
except (binascii.Error, ValueError) as exc:
|
||||
raise MissingCredentialsError(
|
||||
"X-GA-Credentials is not valid base64."
|
||||
) from exc
|
||||
try:
|
||||
service_account_info = json.loads(decoded)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
raise MissingCredentialsError(
|
||||
"X-GA-Credentials does not decode to valid JSON."
|
||||
) from exc
|
||||
if not isinstance(service_account_info, dict):
|
||||
raise MissingCredentialsError(
|
||||
"X-GA-Credentials JSON must be a service-account object."
|
||||
)
|
||||
|
||||
if not access_token and service_account_info is None:
|
||||
raise MissingCredentialsError(
|
||||
"Provide either X-GA-Access-Token or X-GA-Credentials."
|
||||
)
|
||||
# --- Google Search Console ----------------------------------------------------
|
||||
def get_gsc_credentials(
|
||||
x_gsc_access_token: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GSC-Access-Token",
|
||||
description="Ready OAuth2 access token used directly as a Bearer token. "
|
||||
"Takes precedence over X-GSC-Credentials.",
|
||||
),
|
||||
x_gsc_credentials: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GSC-Credentials",
|
||||
description="Base64-encoded Google service-account JSON key. The proxy "
|
||||
"mints a short-lived access token from it (scope webmasters.readonly). "
|
||||
"Used only if X-GSC-Access-Token is absent.",
|
||||
),
|
||||
x_gsc_quota_project: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GSC-Quota-Project",
|
||||
description="Optional Google Cloud project id for quota/billing.",
|
||||
),
|
||||
) -> GoogleCredentials:
|
||||
return _build_google_credentials(
|
||||
x_gsc_access_token,
|
||||
x_gsc_credentials,
|
||||
x_gsc_quota_project,
|
||||
config.GSC_SCOPE,
|
||||
token_header="X-GSC-Access-Token",
|
||||
creds_header="X-GSC-Credentials",
|
||||
)
|
||||
|
||||
return GaCredentials(
|
||||
access_token=access_token,
|
||||
service_account_info=service_account_info,
|
||||
quota_project=(x_ga_quota_project or "").strip() or None,
|
||||
|
||||
# --- Google Ads ---------------------------------------------------------------
|
||||
def get_google_ads_credentials(
|
||||
x_gads_developer_token: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GAds-Developer-Token",
|
||||
description="Google Ads API developer token (from a Google Ads manager "
|
||||
"account). Required for every Google Ads call.",
|
||||
),
|
||||
x_gads_access_token: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GAds-Access-Token",
|
||||
description="Ready OAuth2 access token used directly as a Bearer token. "
|
||||
"Takes precedence over X-GAds-Credentials.",
|
||||
),
|
||||
x_gads_credentials: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GAds-Credentials",
|
||||
description="Base64-encoded Google service-account JSON key (scope "
|
||||
"adwords). Requires domain-wide delegation; usually an access token is "
|
||||
"easier. Used only if X-GAds-Access-Token is absent.",
|
||||
),
|
||||
x_gads_login_customer_id: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GAds-Login-Customer-Id",
|
||||
description="Optional manager (MCC) customer id used as login-customer-id "
|
||||
"header. Digits only, no dashes.",
|
||||
),
|
||||
x_gads_quota_project: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GAds-Quota-Project",
|
||||
description="Optional Google Cloud project id for quota/billing.",
|
||||
),
|
||||
) -> GoogleAdsCredentials:
|
||||
developer_token = (x_gads_developer_token or "").strip()
|
||||
if not developer_token:
|
||||
raise MissingCredentialsError("X-GAds-Developer-Token header is required.")
|
||||
|
||||
google = _build_google_credentials(
|
||||
x_gads_access_token,
|
||||
x_gads_credentials,
|
||||
x_gads_quota_project,
|
||||
config.GOOGLE_ADS_SCOPE,
|
||||
token_header="X-GAds-Access-Token",
|
||||
creds_header="X-GAds-Credentials",
|
||||
)
|
||||
login_customer_id = (x_gads_login_customer_id or "").strip().replace("-", "") or None
|
||||
return GoogleAdsCredentials(
|
||||
google=google,
|
||||
developer_token=developer_token,
|
||||
login_customer_id=login_customer_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user