upt
This commit is contained in:
@@ -52,6 +52,7 @@ Env promenne lze nahradit primo v requestu:
|
|||||||
- `GOOGLE_SERVICE_ACCOUNT_EMAIL`: `serviceAccountEmail` nebo `X-Google-Service-Account-Email`
|
- `GOOGLE_SERVICE_ACCOUNT_EMAIL`: `serviceAccountEmail` nebo `X-Google-Service-Account-Email`
|
||||||
- `GOOGLE_PRIVATE_KEY`: `privateKey` nebo `X-Google-Private-Key`
|
- `GOOGLE_PRIVATE_KEY`: `privateKey` nebo `X-Google-Private-Key`
|
||||||
- `GOOGLE_SCOPES`: `scope`, `scopes` nebo `X-Google-Scopes`
|
- `GOOGLE_SCOPES`: `scope`, `scopes` nebo `X-Google-Scopes`
|
||||||
|
- `GOOGLE_SERVICE_ACCOUNT_JSON`: `serviceAccountJson`, `serviceAccountJsonEnv`, `X-Google-Service-Account-Json`, `X-Google-Service-Account-Json-Env`
|
||||||
- `GOOGLE_ACCESS_TOKEN`: `Authorization: Bearer <token>`, `accessToken`, `accessTokenEnv`, `X-Google-Access-Token`, `X-Google-Access-Token-Env`
|
- `GOOGLE_ACCESS_TOKEN`: `Authorization: Bearer <token>`, `accessToken`, `accessTokenEnv`, `X-Google-Access-Token`, `X-Google-Access-Token-Env`
|
||||||
- `GOOGLE_API_KEY`: `apiKey`, `apiKeyEnv`, `X-Google-Api-Key`, `X-Google-Api-Key-Env`
|
- `GOOGLE_API_KEY`: `apiKey`, `apiKeyEnv`, `X-Google-Api-Key`, `X-Google-Api-Key-Env`
|
||||||
|
|
||||||
@@ -143,6 +144,31 @@ POST /google/sheets/write-by-url
|
|||||||
|
|
||||||
`mode=append` prida radky pod existujici data. `mode=update` prepise bunky od `startCell`.
|
`mode=append` prida radky pod existujici data. `mode=update` prepise bunky od `startCell`.
|
||||||
|
|
||||||
|
Stejny endpoint lze volat primo se service account JSON. V tom pripade se neposila `Authorization` header; sluzba si z `client_email` a `private_key` sama vystavi access token:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"spreadsheetUrl": "https://docs.google.com/spreadsheets/d/1abcDEFghiJKLmnopQRstuVWXyz/edit#gid=0",
|
||||||
|
"sheetName": "Objednavky",
|
||||||
|
"mode": "append",
|
||||||
|
"values": [
|
||||||
|
["2026-06-15", "ACME", 1234]
|
||||||
|
],
|
||||||
|
"serviceAccountScopes": [
|
||||||
|
"https://www.googleapis.com/auth/spreadsheets"
|
||||||
|
],
|
||||||
|
"serviceAccountJson": {
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "project-id",
|
||||||
|
"private_key_id": "key-id",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "service-account@project-id.iam.gserviceaccount.com",
|
||||||
|
"client_id": "123456789",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## OAuth postup
|
## OAuth postup
|
||||||
|
|
||||||
1. Zavolej `GET /google/oauth/scopes` a vyber scopes podle sluzeb.
|
1. Zavolej `GET /google/oauth/scopes` a vyber scopes podle sluzeb.
|
||||||
|
|||||||
@@ -36,9 +36,51 @@ Swagger obsahuje `GET /google/configuration`, ktery vypise podporovane env prome
|
|||||||
- `GOOGLE_SERVICE_ACCOUNT_EMAIL`: `serviceAccountEmail`, `X-Google-Service-Account-Email`
|
- `GOOGLE_SERVICE_ACCOUNT_EMAIL`: `serviceAccountEmail`, `X-Google-Service-Account-Email`
|
||||||
- `GOOGLE_PRIVATE_KEY`: `privateKey`, `X-Google-Private-Key`
|
- `GOOGLE_PRIVATE_KEY`: `privateKey`, `X-Google-Private-Key`
|
||||||
- `GOOGLE_SCOPES`: `scope`, `scopes`, `X-Google-Scopes`
|
- `GOOGLE_SCOPES`: `scope`, `scopes`, `X-Google-Scopes`
|
||||||
|
- `GOOGLE_SERVICE_ACCOUNT_JSON`: `serviceAccountJson`, `serviceAccountJsonEnv`, `X-Google-Service-Account-Json`, `X-Google-Service-Account-Json-Env`
|
||||||
- `GOOGLE_ACCESS_TOKEN`: `Authorization: Bearer <token>`, `accessToken`, `accessTokenEnv`, `X-Google-Access-Token`, `X-Google-Access-Token-Env`
|
- `GOOGLE_ACCESS_TOKEN`: `Authorization: Bearer <token>`, `accessToken`, `accessTokenEnv`, `X-Google-Access-Token`, `X-Google-Access-Token-Env`
|
||||||
- `GOOGLE_API_KEY`: `apiKey`, `apiKeyEnv`, `X-Google-Api-Key`, `X-Google-Api-Key-Env`
|
- `GOOGLE_API_KEY`: `apiKey`, `apiKeyEnv`, `X-Google-Api-Key`, `X-Google-Api-Key-Env`
|
||||||
|
|
||||||
|
### Service account JSON
|
||||||
|
|
||||||
|
Google service account JSON neobsahuje `access_token`. Obsahuje hlavne `client_email`, `private_key` a `token_uri`. Sluzba z techto poli sama vytvori JWT assertion, zavola Google OAuth token endpoint a ziska docasny `access_token`.
|
||||||
|
|
||||||
|
Produktove endpointy proto lze volat primo s:
|
||||||
|
|
||||||
|
- `serviceAccountJson` v body
|
||||||
|
- `X-Google-Service-Account-Json` header
|
||||||
|
- `serviceAccountJsonEnv` nebo `X-Google-Service-Account-Json-Env`, napr. `GOOGLE_SERVICE_ACCOUNT_JSON`
|
||||||
|
|
||||||
|
Je nutne dodat scopes:
|
||||||
|
|
||||||
|
- `serviceAccountScopes` v body
|
||||||
|
- `X-Google-Service-Account-Scopes` header
|
||||||
|
- nebo `GOOGLE_SCOPES`
|
||||||
|
|
||||||
|
Priklad pro zapis do Sheets:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"spreadsheetUrl": "https://docs.google.com/spreadsheets/d/1abcDEFghiJKLmnopQRstuVWXyz/edit#gid=0",
|
||||||
|
"sheetName": "Objednavky",
|
||||||
|
"mode": "append",
|
||||||
|
"values": [
|
||||||
|
["2026-06-15", "ACME", 1234]
|
||||||
|
],
|
||||||
|
"serviceAccountScopes": [
|
||||||
|
"https://www.googleapis.com/auth/spreadsheets"
|
||||||
|
],
|
||||||
|
"serviceAccountJson": {
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "project-id",
|
||||||
|
"private_key_id": "key-id",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "service-account@project-id.iam.gserviceaccount.com",
|
||||||
|
"client_id": "123456789",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### OAuth krok za krokem
|
### OAuth krok za krokem
|
||||||
|
|
||||||
1. Zavolej `GET /google/oauth/scopes`.
|
1. Zavolej `GET /google/oauth/scopes`.
|
||||||
|
|||||||
+257
-7
@@ -15,6 +15,10 @@ type GoogleRequestBody = {
|
|||||||
accessTokenEnv?: string;
|
accessTokenEnv?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
apiKeyEnv?: string;
|
apiKeyEnv?: string;
|
||||||
|
serviceAccountJson?: GoogleServiceAccountJson | string;
|
||||||
|
serviceAccountJsonEnv?: string;
|
||||||
|
serviceAccountScopes?: string[] | string;
|
||||||
|
serviceAccountSubject?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TokenExchangeBody = {
|
type TokenExchangeBody = {
|
||||||
@@ -33,6 +37,18 @@ type ServiceAccountTokenBody = {
|
|||||||
subject?: string;
|
subject?: string;
|
||||||
serviceAccountEmail?: string;
|
serviceAccountEmail?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
|
serviceAccountJson?: GoogleServiceAccountJson | string;
|
||||||
|
serviceAccountJsonEnv?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GoogleServiceAccountJson = {
|
||||||
|
type?: string;
|
||||||
|
project_id?: string;
|
||||||
|
private_key_id?: string;
|
||||||
|
private_key?: string;
|
||||||
|
client_email?: string;
|
||||||
|
client_id?: string;
|
||||||
|
token_uri?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SheetWriteByUrlBody = {
|
type SheetWriteByUrlBody = {
|
||||||
@@ -238,8 +254,17 @@ app.post("/google/oauth/token", async (req, res) => {
|
|||||||
app.post("/google/oauth/service-account-token", async (req, res) => {
|
app.post("/google/oauth/service-account-token", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const body = req.body as ServiceAccountTokenBody;
|
const body = req.body as ServiceAccountTokenBody;
|
||||||
const email = body.serviceAccountEmail || req.header("x-google-service-account-email") || process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
|
const serviceAccountJson = readServiceAccountJson(req, body);
|
||||||
const rawPrivateKey = body.privateKey || req.header("x-google-private-key") || process.env.GOOGLE_PRIVATE_KEY;
|
const email =
|
||||||
|
body.serviceAccountEmail ||
|
||||||
|
serviceAccountJson?.client_email ||
|
||||||
|
req.header("x-google-service-account-email") ||
|
||||||
|
process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
|
||||||
|
const rawPrivateKey =
|
||||||
|
body.privateKey ||
|
||||||
|
serviceAccountJson?.private_key ||
|
||||||
|
req.header("x-google-private-key") ||
|
||||||
|
process.env.GOOGLE_PRIVATE_KEY;
|
||||||
const privateKey = rawPrivateKey?.replace(/\\n/g, "\n");
|
const privateKey = rawPrivateKey?.replace(/\\n/g, "\n");
|
||||||
const scope = body.scope || body.scopes?.join(" ") || req.header("x-google-scopes") || process.env.GOOGLE_SCOPES;
|
const scope = body.scope || body.scopes?.join(" ") || req.header("x-google-scopes") || process.env.GOOGLE_SCOPES;
|
||||||
|
|
||||||
@@ -254,7 +279,7 @@ app.post("/google/oauth/service-account-token", async (req, res) => {
|
|||||||
const claimSet: Record<string, string | number> = {
|
const claimSet: Record<string, string | number> = {
|
||||||
iss: email,
|
iss: email,
|
||||||
scope,
|
scope,
|
||||||
aud: "https://oauth2.googleapis.com/token",
|
aud: serviceAccountJson?.token_uri || "https://oauth2.googleapis.com/token",
|
||||||
iat: now,
|
iat: now,
|
||||||
exp: now + 3600
|
exp: now + 3600
|
||||||
};
|
};
|
||||||
@@ -269,7 +294,7 @@ app.post("/google/oauth/service-account-token", async (req, res) => {
|
|||||||
assertion
|
assertion
|
||||||
});
|
});
|
||||||
|
|
||||||
await pipeGoogleForm(res, "https://oauth2.googleapis.com/token", params);
|
await pipeGoogleForm(res, serviceAccountJson?.token_uri || "https://oauth2.googleapis.com/token", params);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendError(res, error);
|
sendError(res, error);
|
||||||
}
|
}
|
||||||
@@ -320,6 +345,7 @@ app.get("/google/configuration", (_req, res) => {
|
|||||||
{ name: "GOOGLE_SERVICE_ACCOUNT_EMAIL", usedBy: ["/google/oauth/service-account-token"], alternatives: ["serviceAccountEmail", "X-Google-Service-Account-Email"] },
|
{ name: "GOOGLE_SERVICE_ACCOUNT_EMAIL", usedBy: ["/google/oauth/service-account-token"], alternatives: ["serviceAccountEmail", "X-Google-Service-Account-Email"] },
|
||||||
{ name: "GOOGLE_PRIVATE_KEY", usedBy: ["/google/oauth/service-account-token"], alternatives: ["privateKey", "X-Google-Private-Key"] },
|
{ name: "GOOGLE_PRIVATE_KEY", usedBy: ["/google/oauth/service-account-token"], alternatives: ["privateKey", "X-Google-Private-Key"] },
|
||||||
{ name: "GOOGLE_SCOPES", usedBy: ["/google/oauth/service-account-token"], alternatives: ["scope", "scopes", "X-Google-Scopes"] },
|
{ name: "GOOGLE_SCOPES", usedBy: ["/google/oauth/service-account-token"], alternatives: ["scope", "scopes", "X-Google-Scopes"] },
|
||||||
|
{ name: "GOOGLE_SERVICE_ACCOUNT_JSON", usedBy: ["/google/oauth/service-account-token", "Google product endpoints", "/google/request"], alternatives: ["serviceAccountJson", "serviceAccountJsonEnv", "X-Google-Service-Account-Json", "X-Google-Service-Account-Json-Env"] },
|
||||||
{ name: "GOOGLE_ACCESS_TOKEN", usedBy: ["Google product endpoints", "/google/request"], alternatives: ["Authorization: Bearer", "accessToken", "accessTokenEnv", "X-Google-Access-Token", "X-Google-Access-Token-Env"] },
|
{ name: "GOOGLE_ACCESS_TOKEN", usedBy: ["Google product endpoints", "/google/request"], alternatives: ["Authorization: Bearer", "accessToken", "accessTokenEnv", "X-Google-Access-Token", "X-Google-Access-Token-Env"] },
|
||||||
{ name: "GOOGLE_API_KEY", usedBy: ["Google product endpoints", "/google/request"], alternatives: ["apiKey", "apiKeyEnv", "X-Google-Api-Key", "X-Google-Api-Key-Env"] }
|
{ name: "GOOGLE_API_KEY", usedBy: ["Google product endpoints", "/google/request"], alternatives: ["apiKey", "apiKeyEnv", "X-Google-Api-Key", "X-Google-Api-Key-Env"] }
|
||||||
],
|
],
|
||||||
@@ -863,6 +889,7 @@ app.post("/google/request", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const requestBody = req.body as GoogleRequestBody;
|
const requestBody = req.body as GoogleRequestBody;
|
||||||
applyCredentialHeaders(req, requestBody);
|
applyCredentialHeaders(req, requestBody);
|
||||||
|
await applyServiceAccountAuthorization(req, requestBody);
|
||||||
const targetUrl = buildGoogleUrl(requestBody);
|
const targetUrl = buildGoogleUrl(requestBody);
|
||||||
const headers = buildGoogleHeaders(req, requestBody);
|
const headers = buildGoogleHeaders(req, requestBody);
|
||||||
const method = (requestBody.method || "GET").toUpperCase();
|
const method = (requestBody.method || "GET").toUpperCase();
|
||||||
@@ -919,13 +946,14 @@ async function callGoogle(req: Request, res: Response, options: GoogleRouteOptio
|
|||||||
};
|
};
|
||||||
|
|
||||||
applyCredentialHeaders(req, requestBody);
|
applyCredentialHeaders(req, requestBody);
|
||||||
|
await applyServiceAccountAuthorization(req, requestBody);
|
||||||
const targetUrl = buildGoogleUrl(requestBody);
|
const targetUrl = buildGoogleUrl(requestBody);
|
||||||
const headers = buildGoogleHeaders(req, requestBody);
|
const headers = buildGoogleHeaders(req, requestBody);
|
||||||
const method = (requestBody.method || "GET").toUpperCase();
|
const method = (requestBody.method || "GET").toUpperCase();
|
||||||
const init: RequestInit = { method, headers };
|
const init: RequestInit = { method, headers };
|
||||||
|
|
||||||
if (!["GET", "HEAD"].includes(method) && options.body !== undefined) {
|
if (!["GET", "HEAD"].includes(method) && options.body !== undefined) {
|
||||||
init.body = JSON.stringify(options.body);
|
init.body = JSON.stringify(stripCredentialFields(options.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(targetUrl, init);
|
const response = await fetch(targetUrl, init);
|
||||||
@@ -958,11 +986,102 @@ function readBodyString(body: unknown, key: string): string | undefined {
|
|||||||
return typeof value === "string" ? value : undefined;
|
return typeof value === "string" ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripCredentialFields(body: JsonValue): JsonValue {
|
||||||
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentialFields = new Set([
|
||||||
|
"accessToken",
|
||||||
|
"accessTokenEnv",
|
||||||
|
"apiKey",
|
||||||
|
"apiKeyEnv",
|
||||||
|
"serviceAccountJson",
|
||||||
|
"serviceAccountJsonEnv",
|
||||||
|
"serviceAccountScopes",
|
||||||
|
"serviceAccountSubject"
|
||||||
|
]);
|
||||||
|
const result: Record<string, JsonValue> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(body)) {
|
||||||
|
if (!credentialFields.has(key)) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function applyCredentialHeaders(req: Request, body: GoogleRequestBody): void {
|
function applyCredentialHeaders(req: Request, body: GoogleRequestBody): void {
|
||||||
body.accessToken ||= req.header("x-google-access-token");
|
body.accessToken ||= req.header("x-google-access-token");
|
||||||
body.accessTokenEnv ||= req.header("x-google-access-token-env");
|
body.accessTokenEnv ||= req.header("x-google-access-token-env");
|
||||||
body.apiKey ||= req.header("x-google-api-key");
|
body.apiKey ||= req.header("x-google-api-key");
|
||||||
body.apiKeyEnv ||= req.header("x-google-api-key-env");
|
body.apiKeyEnv ||= req.header("x-google-api-key-env");
|
||||||
|
body.serviceAccountJson ||= req.header("x-google-service-account-json");
|
||||||
|
body.serviceAccountJsonEnv ||= req.header("x-google-service-account-json-env");
|
||||||
|
body.serviceAccountScopes ||= req.header("x-google-service-account-scopes");
|
||||||
|
body.serviceAccountSubject ||= req.header("x-google-service-account-subject");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyServiceAccountAuthorization(req: Request, body: GoogleRequestBody): Promise<void> {
|
||||||
|
if (body.accessToken || body.accessTokenEnv || readBearerToken(req)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceAccountJson = readServiceAccountJson(req, body);
|
||||||
|
if (!serviceAccountJson) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopes = readServiceAccountScopes(req, body);
|
||||||
|
if (scopes.length === 0) {
|
||||||
|
throw new Error("Service account authentication requires serviceAccountScopes, X-Google-Service-Account-Scopes, or GOOGLE_SCOPES.");
|
||||||
|
}
|
||||||
|
|
||||||
|
body.accessToken = await createServiceAccountAccessToken(
|
||||||
|
serviceAccountJson,
|
||||||
|
scopes.join(" "),
|
||||||
|
body.serviceAccountSubject || req.header("x-google-service-account-subject") || undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readServiceAccountJson(req: Request, body?: GoogleRequestBody | ServiceAccountTokenBody): GoogleServiceAccountJson | undefined {
|
||||||
|
const bodyValue = body && "serviceAccountJson" in body ? body.serviceAccountJson : undefined;
|
||||||
|
const envName = body && "serviceAccountJsonEnv" in body ? body.serviceAccountJsonEnv : undefined;
|
||||||
|
const raw =
|
||||||
|
bodyValue ||
|
||||||
|
req.header("x-google-service-account-json") ||
|
||||||
|
readEnvValue(envName) ||
|
||||||
|
readEnvValue(req.header("x-google-service-account-json-env")) ||
|
||||||
|
process.env.GOOGLE_SERVICE_ACCOUNT_JSON;
|
||||||
|
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof raw === "object") {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as GoogleServiceAccountJson;
|
||||||
|
} catch {
|
||||||
|
throw new Error("serviceAccountJson must be a valid Google service account JSON object or JSON string.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readServiceAccountScopes(req: Request, body: GoogleRequestBody): string[] {
|
||||||
|
const value =
|
||||||
|
body.serviceAccountScopes ||
|
||||||
|
readBodyString(req.body, "serviceAccountScopes") ||
|
||||||
|
req.header("x-google-service-account-scopes") ||
|
||||||
|
process.env.GOOGLE_SCOPES;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseScopes(typeof value === "string" ? value : undefined) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireQueryString(req: Request, res: Response, key: string): string | undefined {
|
function requireQueryString(req: Request, res: Response, key: string): string | undefined {
|
||||||
@@ -1102,6 +1221,69 @@ async function pipeGoogleForm(res: Response, url: string, params: URLSearchParam
|
|||||||
await relayResponse(res, response);
|
await relayResponse(res, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createServiceAccountAccessToken(
|
||||||
|
serviceAccountJson: GoogleServiceAccountJson,
|
||||||
|
scope: string,
|
||||||
|
subject?: string
|
||||||
|
): Promise<string> {
|
||||||
|
const email = serviceAccountJson.client_email;
|
||||||
|
const rawPrivateKey = serviceAccountJson.private_key;
|
||||||
|
const privateKey = rawPrivateKey?.replace(/\\n/g, "\n");
|
||||||
|
const tokenUri = serviceAccountJson.token_uri || "https://oauth2.googleapis.com/token";
|
||||||
|
|
||||||
|
if (!email || !privateKey) {
|
||||||
|
throw new Error("Google service account JSON must contain client_email and private_key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const claimSet: Record<string, string | number> = {
|
||||||
|
iss: email,
|
||||||
|
scope,
|
||||||
|
aud: tokenUri,
|
||||||
|
iat: now,
|
||||||
|
exp: now + 3600
|
||||||
|
};
|
||||||
|
|
||||||
|
if (subject) {
|
||||||
|
claimSet.sub = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertion = signJwt({ alg: "RS256", typ: "JWT" }, claimSet, privateKey);
|
||||||
|
const response = await fetch(tokenUri, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
"content-type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||||
|
assertion
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
let payload: unknown;
|
||||||
|
try {
|
||||||
|
payload = text ? JSON.parse(text) : {};
|
||||||
|
} catch {
|
||||||
|
payload = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Service account token request failed with HTTP ${response.status}: ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = typeof payload === "object" && payload && "access_token" in payload
|
||||||
|
? (payload as Record<string, unknown>).access_token
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (typeof accessToken !== "string" || !accessToken) {
|
||||||
|
throw new Error("Service account token response did not contain access_token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
async function relayResponse(res: Response, response: globalThis.Response): Promise<void> {
|
async function relayResponse(res: Response, response: globalThis.Response): Promise<void> {
|
||||||
const contentType = response.headers.get("content-type") || "application/json";
|
const contentType = response.headers.get("content-type") || "application/json";
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -1438,6 +1620,8 @@ function buildOpenApiSchemas(): Record<string, unknown> {
|
|||||||
properties: {
|
properties: {
|
||||||
serviceAccountEmail: { type: "string", description: "Alternativa k X-Google-Service-Account-Email nebo env GOOGLE_SERVICE_ACCOUNT_EMAIL." },
|
serviceAccountEmail: { type: "string", description: "Alternativa k X-Google-Service-Account-Email nebo env GOOGLE_SERVICE_ACCOUNT_EMAIL." },
|
||||||
privateKey: { type: "string", description: "Alternativa k X-Google-Private-Key nebo env GOOGLE_PRIVATE_KEY. Podporuje \\n escapovani." },
|
privateKey: { type: "string", description: "Alternativa k X-Google-Private-Key nebo env GOOGLE_PRIVATE_KEY. Podporuje \\n escapovani." },
|
||||||
|
serviceAccountJson: { $ref: "#/components/schemas/GoogleServiceAccountJson" },
|
||||||
|
serviceAccountJsonEnv: { type: "string", example: "GOOGLE_SERVICE_ACCOUNT_JSON" },
|
||||||
scopes: { type: "array", items: { type: "string" }, description: "Alternativa k X-Google-Scopes nebo env GOOGLE_SCOPES." },
|
scopes: { type: "array", items: { type: "string" }, description: "Alternativa k X-Google-Scopes nebo env GOOGLE_SCOPES." },
|
||||||
scope: { type: "string" },
|
scope: { type: "string" },
|
||||||
subject: { type: "string", description: "Workspace user pro domain-wide delegation." }
|
subject: { type: "string", description: "Workspace user pro domain-wide delegation." }
|
||||||
@@ -1456,7 +1640,31 @@ function buildOpenApiSchemas(): Record<string, unknown> {
|
|||||||
accessToken: { type: "string", description: "Alternativa k Authorization Bearer nebo X-Google-Access-Token." },
|
accessToken: { type: "string", description: "Alternativa k Authorization Bearer nebo X-Google-Access-Token." },
|
||||||
accessTokenEnv: { type: "string", example: "GOOGLE_ACCESS_TOKEN", description: "Jmeno env promenne s access tokenem." },
|
accessTokenEnv: { type: "string", example: "GOOGLE_ACCESS_TOKEN", description: "Jmeno env promenne s access tokenem." },
|
||||||
apiKey: { type: "string", description: "Alternativa k X-Google-Api-Key nebo GOOGLE_API_KEY." },
|
apiKey: { type: "string", description: "Alternativa k X-Google-Api-Key nebo GOOGLE_API_KEY." },
|
||||||
apiKeyEnv: { type: "string", example: "GOOGLE_API_KEY", description: "Jmeno env promenne s API key." }
|
apiKeyEnv: { type: "string", example: "GOOGLE_API_KEY", description: "Jmeno env promenne s API key." },
|
||||||
|
serviceAccountJson: { $ref: "#/components/schemas/GoogleServiceAccountJson" },
|
||||||
|
serviceAccountJsonEnv: { type: "string", example: "GOOGLE_SERVICE_ACCOUNT_JSON" },
|
||||||
|
serviceAccountScopes: {
|
||||||
|
oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
||||||
|
description: "Scopes pro automaticke vystaveni tokenu ze service account JSON."
|
||||||
|
},
|
||||||
|
serviceAccountSubject: { type: "string", description: "Volitelny Workspace uzivatel pro domain-wide delegation." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GoogleServiceAccountJson: {
|
||||||
|
type: "object",
|
||||||
|
required: ["client_email", "private_key"],
|
||||||
|
properties: {
|
||||||
|
type: { type: "string", example: "service_account" },
|
||||||
|
project_id: { type: "string" },
|
||||||
|
private_key_id: { type: "string" },
|
||||||
|
private_key: { type: "string", description: "Private key ze service account JSON. Secret se nevraci ve vystupu." },
|
||||||
|
client_email: { type: "string", description: "Service account email pouzity jako JWT iss." },
|
||||||
|
client_id: { type: "string" },
|
||||||
|
auth_uri: { type: "string" },
|
||||||
|
token_uri: { type: "string", example: "https://oauth2.googleapis.com/token" },
|
||||||
|
auth_provider_x509_cert_url: { type: "string" },
|
||||||
|
client_x509_cert_url: { type: "string" },
|
||||||
|
universe_domain: { type: "string", example: "googleapis.com" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Calendar: {
|
Calendar: {
|
||||||
@@ -1566,7 +1774,14 @@ function buildOpenApiSchemas(): Record<string, unknown> {
|
|||||||
example: [["Datum", "Castka"], ["2026-06-15", 1234]]
|
example: [["Datum", "Castka"], ["2026-06-15", 1234]]
|
||||||
},
|
},
|
||||||
accessToken: { type: "string", description: "Volitelne. Lepsi je poslat Authorization: Bearer <token>." },
|
accessToken: { type: "string", description: "Volitelne. Lepsi je poslat Authorization: Bearer <token>." },
|
||||||
accessTokenEnv: { type: "string", description: "Volitelne jmeno env var s access tokenem." }
|
accessTokenEnv: { type: "string", description: "Volitelne jmeno env var s access tokenem." },
|
||||||
|
serviceAccountJson: { $ref: "#/components/schemas/GoogleServiceAccountJson" },
|
||||||
|
serviceAccountJsonEnv: { type: "string", example: "GOOGLE_SERVICE_ACCOUNT_JSON" },
|
||||||
|
serviceAccountScopes: {
|
||||||
|
oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
||||||
|
example: ["https://www.googleapis.com/auth/spreadsheets"],
|
||||||
|
description: "Scopes pro automaticke vystaveni tokenu ze service account JSON."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SheetWriteResponse: {
|
SheetWriteResponse: {
|
||||||
@@ -1832,6 +2047,34 @@ function googleCredentialHeaderParameters(): Record<string, unknown>[] {
|
|||||||
required: false,
|
required: false,
|
||||||
schema: { type: "string", example: "GOOGLE_API_KEY" },
|
schema: { type: "string", example: "GOOGLE_API_KEY" },
|
||||||
description: "Jmeno env promenne obsahujici Google API key. Hodnota env se nevraci ve vystupu."
|
description: "Jmeno env promenne obsahujici Google API key. Hodnota env se nevraci ve vystupu."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Google-Service-Account-Json",
|
||||||
|
in: "header",
|
||||||
|
required: false,
|
||||||
|
schema: { type: "string" },
|
||||||
|
description: "Cely Google service account JSON jako string. Sluzba z nej vystavi access token automaticky. Secret se nevraci ve vystupu."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Google-Service-Account-Json-Env",
|
||||||
|
in: "header",
|
||||||
|
required: false,
|
||||||
|
schema: { type: "string", example: "GOOGLE_SERVICE_ACCOUNT_JSON" },
|
||||||
|
description: "Jmeno env promenne obsahujici cely service account JSON."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Google-Service-Account-Scopes",
|
||||||
|
in: "header",
|
||||||
|
required: false,
|
||||||
|
schema: { type: "string", example: "https://www.googleapis.com/auth/spreadsheets" },
|
||||||
|
description: "Scopes pro automaticke vystaveni tokenu ze service account JSON."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Google-Service-Account-Subject",
|
||||||
|
in: "header",
|
||||||
|
required: false,
|
||||||
|
schema: { type: "string" },
|
||||||
|
description: "Volitelny Workspace uzivatel pro domain-wide delegation."
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -1896,6 +2139,13 @@ function serviceAccountHeaderParameters(): Record<string, unknown>[] {
|
|||||||
required: false,
|
required: false,
|
||||||
schema: { type: "string" },
|
schema: { type: "string" },
|
||||||
description: "Alternativa k body.scope/body.scopes nebo env GOOGLE_SCOPES."
|
description: "Alternativa k body.scope/body.scopes nebo env GOOGLE_SCOPES."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Google-Service-Account-Json",
|
||||||
|
in: "header",
|
||||||
|
required: false,
|
||||||
|
schema: { type: "string" },
|
||||||
|
description: "Cely Google service account JSON jako string. Alternativa k body.serviceAccountJson nebo env GOOGLE_SERVICE_ACCOUNT_JSON."
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user