feat: align oauth flows with uber auth docs and add client-credentials token caching
This commit is contained in:
parent
fccabf449a
commit
042dc1686e
@ -8,3 +8,8 @@ Focus: Uber Eats OAuth 2.0 per merchant (multi-client onboarding).
|
|||||||
- Token expiry handling policy
|
- Token expiry handling policy
|
||||||
- Sandbox utility for app token generation (`client_credentials`)
|
- Sandbox utility for app token generation (`client_credentials`)
|
||||||
- Domain pairing validation to prevent sandbox/production mismatch
|
- Domain pairing validation to prevent sandbox/production mismatch
|
||||||
|
|
||||||
|
Grant usage policy:
|
||||||
|
|
||||||
|
- `client_credentials`: Store/Menu/Order/Reporting regular operations
|
||||||
|
- `authorization_code` (`eats.pos_provisioning`): store discovery/integration activation
|
||||||
|
|||||||
30
docs/developer-portal/02-authentication-audit.md
Normal file
30
docs/developer-portal/02-authentication-audit.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# 02 Authentication Audit
|
||||||
|
|
||||||
|
Source checked: Uber Eats "Authentication" section shared by you.
|
||||||
|
|
||||||
|
## Implemented Now
|
||||||
|
|
||||||
|
- OAuth endpoints aligned to `auth.uber.com` in environment defaults.
|
||||||
|
- Authorization URL default scope changed to `eats.pos_provisioning` (authorization_code flow use case).
|
||||||
|
- Added cached `client_credentials` token retrieval to reduce token churn.
|
||||||
|
- Added token request tracking and soft guard near rate limit (`100/hour`).
|
||||||
|
- Added domain pairing status endpoint:
|
||||||
|
- `GET /api/v1/auth/uber/domain-pairing-status`
|
||||||
|
- Added auth capabilities endpoint listing grant types/scopes/rate metadata:
|
||||||
|
- `GET /api/v1/auth/uber/capabilities`
|
||||||
|
- Updated proxy auth model:
|
||||||
|
- default regular API calls use app-level `client_credentials` token
|
||||||
|
- optional `authMode=merchant` for merchant OAuth token calls
|
||||||
|
|
||||||
|
## Existing From Earlier
|
||||||
|
|
||||||
|
- Authorization code callback exchange
|
||||||
|
- Merchant token refresh route
|
||||||
|
- Manual merchant connection storage
|
||||||
|
|
||||||
|
## Pending / Needs More Official Docs
|
||||||
|
|
||||||
|
- Exact endpoint-by-endpoint scope mapping table enforcement in code
|
||||||
|
- Full activation/provisioning flow routes (beyond auth callback)
|
||||||
|
- Token revocation handling if Uber publishes endpoint/process details
|
||||||
|
|
||||||
@ -63,7 +63,7 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/auth/uber/client-credentials-token": {
|
"/api/v1/auth/uber/client-credentials-token": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Generate Uber client credentials token (sandbox/testing utility)",
|
"summary": "Get cached or live Uber client credentials token",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
@ -74,6 +74,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/auth/uber/capabilities": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Get Uber auth grants, scopes, and token-rate-limit metadata",
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Auth capability details"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/auth/uber/{merchantId}/refresh-token": {
|
"/api/v1/auth/uber/{merchantId}/refresh-token": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Refresh Uber token for merchant",
|
"summary": "Refresh Uber token for merchant",
|
||||||
|
|||||||
@ -108,6 +108,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Auth Capabilities",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "x-api-key",
|
||||||
|
"value": "{{apiKey}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/v1/auth/uber/capabilities",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"auth",
|
||||||
|
"uber",
|
||||||
|
"capabilities"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Client Credentials Token",
|
"name": "Client Credentials Token",
|
||||||
"request": {
|
"request": {
|
||||||
@ -124,7 +149,7 @@
|
|||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"scope\": \"eats.order\"\n}"
|
"raw": "{\n \"scope\": \"eats.store eats.order\"\n}"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}/api/v1/auth/uber/client-credentials-token",
|
"raw": "{{baseUrl}}/api/v1/auth/uber/client-credentials-token",
|
||||||
@ -190,7 +215,7 @@
|
|||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"method\": \"GET\",\n \"path\": \"/v1/eats/stores/{{storeId}}/orders\"\n}"
|
"raw": "{\n \"method\": \"GET\",\n \"path\": \"/v1/eats/stores/{{storeId}}/orders\",\n \"authMode\": \"app\",\n \"scopes\": \"eats.order\"\n}"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/request",
|
"raw": "{{baseUrl}}/api/v1/uber/request",
|
||||||
|
|||||||
@ -9,6 +9,7 @@ module.exports = {
|
|||||||
merchantRepository: repositories.merchantRepository,
|
merchantRepository: repositories.merchantRepository,
|
||||||
uberConnectionRepository: repositories.uberConnectionRepository,
|
uberConnectionRepository: repositories.uberConnectionRepository,
|
||||||
webhookRepository: repositories.webhookRepository,
|
webhookRepository: repositories.webhookRepository,
|
||||||
apiLogRepository: repositories.apiLogRepository
|
apiLogRepository: repositories.apiLogRepository,
|
||||||
|
appTokenRepository: repositories.appTokenRepository,
|
||||||
|
tokenRequestLogRepository: repositories.tokenRequestLogRepository
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -191,9 +191,120 @@ const apiLogRepository = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appTokenRepository = {
|
||||||
|
findValid({ provider, grantType, scope, minValiditySeconds = 120 }) {
|
||||||
|
const row = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM app_tokens
|
||||||
|
WHERE provider = ?
|
||||||
|
AND grant_type = ?
|
||||||
|
AND scope = ?
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(provider, grantType, scope);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minExpiry = Date.now() + minValiditySeconds * 1000;
|
||||||
|
const expiresAtMs = new Date(row.expires_at).getTime();
|
||||||
|
if (Number.isNaN(expiresAtMs) || expiresAtMs <= minExpiry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
|
||||||
|
upsert({ provider, grantType, scope, accessToken, tokenType, expiresAt }) {
|
||||||
|
const existing = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT * FROM app_tokens
|
||||||
|
WHERE provider = ? AND grant_type = ? AND scope = ?
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(provider, grantType, scope);
|
||||||
|
const timestamp = nowIso();
|
||||||
|
const row = {
|
||||||
|
id: existing?.id || uuidv4(),
|
||||||
|
provider,
|
||||||
|
grant_type: grantType,
|
||||||
|
scope,
|
||||||
|
access_token: accessToken,
|
||||||
|
token_type: tokenType || "Bearer",
|
||||||
|
expires_at: expiresAt,
|
||||||
|
created_at: existing?.created_at || timestamp,
|
||||||
|
updated_at: timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO app_tokens (
|
||||||
|
id, provider, grant_type, scope, access_token, token_type, expires_at, created_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@id, @provider, @grant_type, @scope, @access_token, @token_type, @expires_at, @created_at, @updated_at
|
||||||
|
)
|
||||||
|
ON CONFLICT(provider, grant_type, scope) DO UPDATE SET
|
||||||
|
access_token = excluded.access_token,
|
||||||
|
token_type = excluded.token_type,
|
||||||
|
expires_at = excluded.expires_at,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
`
|
||||||
|
).run(row);
|
||||||
|
|
||||||
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM app_tokens
|
||||||
|
WHERE provider = ? AND grant_type = ? AND scope = ?
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(provider, grantType, scope);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenRequestLogRepository = {
|
||||||
|
insert({ provider, grantType }) {
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO token_request_logs (
|
||||||
|
id, provider, grant_type, requested_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
).run(uuidv4(), provider, grantType, nowIso());
|
||||||
|
},
|
||||||
|
|
||||||
|
countInLastHour({ provider, grantType }) {
|
||||||
|
const sinceIso = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
||||||
|
const row = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT COUNT(*) AS total
|
||||||
|
FROM token_request_logs
|
||||||
|
WHERE provider = ?
|
||||||
|
AND grant_type = ?
|
||||||
|
AND requested_at >= ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(provider, grantType, sinceIso);
|
||||||
|
return row?.total || 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
merchantRepository,
|
merchantRepository,
|
||||||
uberConnectionRepository,
|
uberConnectionRepository,
|
||||||
webhookRepository,
|
webhookRepository,
|
||||||
apiLogRepository
|
apiLogRepository,
|
||||||
|
appTokenRepository,
|
||||||
|
tokenRequestLogRepository
|
||||||
};
|
};
|
||||||
|
|||||||
@ -65,6 +65,28 @@ function initSchema() {
|
|||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
|
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS app_tokens (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
provider TEXT NOT NULL,
|
||||||
|
grant_type TEXT NOT NULL,
|
||||||
|
scope TEXT NOT NULL,
|
||||||
|
access_token TEXT NOT NULL,
|
||||||
|
token_type TEXT NOT NULL,
|
||||||
|
expires_at TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_app_tokens_provider_grant_scope
|
||||||
|
ON app_tokens(provider, grant_type, scope);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS token_request_logs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
provider TEXT NOT NULL,
|
||||||
|
grant_type TEXT NOT NULL,
|
||||||
|
requested_at TEXT NOT NULL
|
||||||
|
);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,4 +94,3 @@ module.exports = {
|
|||||||
db,
|
db,
|
||||||
initSchema
|
initSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,11 @@ const {
|
|||||||
buildAuthorizeUrl,
|
buildAuthorizeUrl,
|
||||||
exchangeCodeForToken,
|
exchangeCodeForToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
getClientCredentialsToken,
|
getCachedClientCredentialsToken,
|
||||||
getDomainMappingStatus
|
getDomainMappingStatus,
|
||||||
|
AUTH_SCOPES
|
||||||
} = require("./auth.service");
|
} = require("./auth.service");
|
||||||
|
const { tokenRequestLogRepository } = require("../../db/adapter");
|
||||||
|
|
||||||
function parseExpiresAt(expiresInSeconds) {
|
function parseExpiresAt(expiresInSeconds) {
|
||||||
if (!expiresInSeconds) {
|
if (!expiresInSeconds) {
|
||||||
@ -91,11 +93,11 @@ async function refreshMerchantToken(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateClientCredentialsToken(req, res) {
|
async function generateClientCredentialsToken(req, res) {
|
||||||
const scope = req.body?.scope || "eats.order";
|
const scope = req.body?.scope || `${AUTH_SCOPES.STORE} ${AUTH_SCOPES.ORDER}`;
|
||||||
const tokenData = await getClientCredentialsToken({ scope });
|
const tokenData = await getCachedClientCredentialsToken({ scope });
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Client credentials token generated",
|
message: "Client credentials token available",
|
||||||
data: tokenData
|
data: tokenData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -112,10 +114,37 @@ async function getDomainPairingStatus(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAuthCapabilities(req, res) {
|
||||||
|
const requestsLastHour = tokenRequestLogRepository.countInLastHour({
|
||||||
|
provider: "uber",
|
||||||
|
grantType: "client_credentials"
|
||||||
|
});
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
tokenEndpoint: "https://auth.uber.com/oauth/v2/token",
|
||||||
|
authorizeEndpoint: "https://auth.uber.com/oauth/v2/authorize",
|
||||||
|
grantTypes: {
|
||||||
|
client_credentials: [
|
||||||
|
AUTH_SCOPES.STORE,
|
||||||
|
AUTH_SCOPES.STORE_STATUS_WRITE,
|
||||||
|
AUTH_SCOPES.ORDER,
|
||||||
|
AUTH_SCOPES.STORE_ORDERS_READ,
|
||||||
|
AUTH_SCOPES.REPORT
|
||||||
|
],
|
||||||
|
authorization_code: [AUTH_SCOPES.POS_PROVISIONING]
|
||||||
|
},
|
||||||
|
clientCredentialsRateLimitPerHour: 100,
|
||||||
|
observedClientCredentialsRequestsLastHour: requestsLastHour
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAuthorizeUrl,
|
getAuthorizeUrl,
|
||||||
oauthCallback,
|
oauthCallback,
|
||||||
refreshMerchantToken,
|
refreshMerchantToken,
|
||||||
generateClientCredentialsToken,
|
generateClientCredentialsToken,
|
||||||
getDomainPairingStatus
|
getDomainPairingStatus,
|
||||||
|
getAuthCapabilities
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,27 @@
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const env = require("../../config/env");
|
const env = require("../../config/env");
|
||||||
|
const { appTokenRepository, tokenRequestLogRepository } = require("../../db/adapter");
|
||||||
|
|
||||||
|
const AUTH_GRANT_TYPES = {
|
||||||
|
CLIENT_CREDENTIALS: "client_credentials",
|
||||||
|
AUTHORIZATION_CODE: "authorization_code"
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTH_SCOPES = {
|
||||||
|
STORE: "eats.store",
|
||||||
|
STORE_STATUS_WRITE: "eats.store.status.write",
|
||||||
|
ORDER: "eats.order",
|
||||||
|
STORE_ORDERS_READ: "eats.store.orders.read",
|
||||||
|
REPORT: "eats.report",
|
||||||
|
POS_PROVISIONING: "eats.pos_provisioning"
|
||||||
|
};
|
||||||
|
|
||||||
const uberAuthClient = axios.create({
|
const uberAuthClient = axios.create({
|
||||||
baseURL: env.UBER_OAUTH_BASE_URL,
|
baseURL: env.UBER_OAUTH_BASE_URL,
|
||||||
timeout: 20000
|
timeout: 20000
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildAuthorizeUrl({ state, scope = "eats.store eats.order" }) {
|
function buildAuthorizeUrl({ state, scope = AUTH_SCOPES.POS_PROVISIONING }) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: env.UBER_CLIENT_ID,
|
client_id: env.UBER_CLIENT_ID,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
@ -70,6 +85,60 @@ async function getClientCredentialsToken({ scope = "eats.order" } = {}) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseExpiresAt(expiresInSeconds) {
|
||||||
|
const seconds = Number(expiresInSeconds || 0);
|
||||||
|
if (!seconds || Number.isNaN(seconds)) {
|
||||||
|
return new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
||||||
|
}
|
||||||
|
return new Date(Date.now() + seconds * 1000).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCachedClientCredentialsToken({ scope }) {
|
||||||
|
const normalizedScope = (scope || AUTH_SCOPES.ORDER).trim().split(/\s+/).sort().join(" ");
|
||||||
|
const cached = appTokenRepository.findValid({
|
||||||
|
provider: "uber",
|
||||||
|
grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS,
|
||||||
|
scope: normalizedScope
|
||||||
|
});
|
||||||
|
if (cached) {
|
||||||
|
return {
|
||||||
|
access_token: cached.access_token,
|
||||||
|
token_type: cached.token_type,
|
||||||
|
scope: cached.scope,
|
||||||
|
expires_in: Math.max(0, Math.floor((new Date(cached.expires_at).getTime() - Date.now()) / 1000)),
|
||||||
|
source: "cache"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestsLastHour = tokenRequestLogRepository.countInLastHour({
|
||||||
|
provider: "uber",
|
||||||
|
grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS
|
||||||
|
});
|
||||||
|
if (requestsLastHour >= 95) {
|
||||||
|
const error = new Error(
|
||||||
|
"Client credentials token request rate is approaching the Uber limit (100/hour). Reuse cached tokens."
|
||||||
|
);
|
||||||
|
error.status = 429;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fresh = await getClientCredentialsToken({ scope: normalizedScope });
|
||||||
|
const expiresAt = parseExpiresAt(fresh.expires_in);
|
||||||
|
appTokenRepository.upsert({
|
||||||
|
provider: "uber",
|
||||||
|
grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS,
|
||||||
|
scope: normalizedScope,
|
||||||
|
accessToken: fresh.access_token,
|
||||||
|
tokenType: fresh.token_type || "Bearer",
|
||||||
|
expiresAt
|
||||||
|
});
|
||||||
|
tokenRequestLogRepository.insert({
|
||||||
|
provider: "uber",
|
||||||
|
grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS
|
||||||
|
});
|
||||||
|
return { ...fresh, source: "live" };
|
||||||
|
}
|
||||||
|
|
||||||
function hostFromUrl(urlValue) {
|
function hostFromUrl(urlValue) {
|
||||||
return new URL(urlValue).host.toLowerCase();
|
return new URL(urlValue).host.toLowerCase();
|
||||||
}
|
}
|
||||||
@ -103,5 +172,8 @@ module.exports = {
|
|||||||
exchangeCodeForToken,
|
exchangeCodeForToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
getClientCredentialsToken,
|
getClientCredentialsToken,
|
||||||
getDomainMappingStatus
|
getDomainMappingStatus,
|
||||||
|
getCachedClientCredentialsToken,
|
||||||
|
AUTH_SCOPES,
|
||||||
|
AUTH_GRANT_TYPES
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,9 +2,11 @@ const { z } = require("zod");
|
|||||||
const proxyService = require("./proxy.service");
|
const proxyService = require("./proxy.service");
|
||||||
|
|
||||||
const genericSchema = z.object({
|
const genericSchema = z.object({
|
||||||
merchantId: z.string().min(1),
|
merchantId: z.string().min(1).optional(),
|
||||||
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
|
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
|
||||||
path: z.string().min(1),
|
path: z.string().min(1),
|
||||||
|
authMode: z.enum(["app", "merchant"]).optional(),
|
||||||
|
scopes: z.string().optional(),
|
||||||
query: z.record(z.string(), z.any()).optional(),
|
query: z.record(z.string(), z.any()).optional(),
|
||||||
body: z.any().optional()
|
body: z.any().optional()
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const axios = require("axios");
|
|||||||
const env = require("../../config/env");
|
const env = require("../../config/env");
|
||||||
const uberEndpoints = require("../../config/uberEndpoints");
|
const uberEndpoints = require("../../config/uberEndpoints");
|
||||||
const { uberConnectionRepository, apiLogRepository } = require("../../db/adapter");
|
const { uberConnectionRepository, apiLogRepository } = require("../../db/adapter");
|
||||||
|
const { getCachedClientCredentialsToken, AUTH_SCOPES } = require("../auth/auth.service");
|
||||||
|
|
||||||
const uberApiClient = axios.create({
|
const uberApiClient = axios.create({
|
||||||
baseURL: env.UBER_API_BASE_URL,
|
baseURL: env.UBER_API_BASE_URL,
|
||||||
@ -16,17 +17,44 @@ function interpolatePath(pathTemplate, params = {}) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAuthHeader(connection) {
|
function buildAuthHeader(tokenType, accessToken) {
|
||||||
return `${connection.token_type || "Bearer"} ${connection.access_token}`;
|
return `${tokenType || "Bearer"} ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callUberApi({ merchantId, method, uberPath, query, body, wrapperRoute }) {
|
function getMerchantConnectionToken(merchantId) {
|
||||||
const connection = uberConnectionRepository.findByMerchantId(merchantId);
|
const connection = uberConnectionRepository.findByMerchantId(merchantId);
|
||||||
if (!connection || connection.status !== "active") {
|
if (!connection || connection.status !== "active") {
|
||||||
const error = new Error("Active Uber connection not found for merchant");
|
const error = new Error("Active Uber merchant OAuth connection not found");
|
||||||
error.status = 404;
|
error.status = 404;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
tokenType: connection.token_type || "Bearer",
|
||||||
|
accessToken: connection.access_token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveAuthToken({ authMode = "app", merchantId, scopes }) {
|
||||||
|
if (authMode === "merchant") {
|
||||||
|
if (!merchantId) {
|
||||||
|
const error = new Error("merchantId is required when authMode=merchant");
|
||||||
|
error.status = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return getMerchantConnectionToken(merchantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await getCachedClientCredentialsToken({
|
||||||
|
scope: scopes || AUTH_SCOPES.ORDER
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
tokenType: tokenData.token_type || "Bearer",
|
||||||
|
accessToken: tokenData.access_token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callUberApi({ merchantId, method, uberPath, query, body, wrapperRoute, authMode, scopes }) {
|
||||||
|
const resolvedAuth = await resolveAuthToken({ authMode, merchantId, scopes });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await uberApiClient.request({
|
const response = await uberApiClient.request({
|
||||||
@ -35,7 +63,7 @@ async function callUberApi({ merchantId, method, uberPath, query, body, wrapperR
|
|||||||
params: query,
|
params: query,
|
||||||
data: body,
|
data: body,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: buildAuthHeader(connection),
|
Authorization: buildAuthHeader(resolvedAuth.tokenType, resolvedAuth.accessToken),
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -72,14 +100,16 @@ async function callUberApi({ merchantId, method, uberPath, query, body, wrapperR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function genericProxy({ merchantId, method, path, query, body }) {
|
async function genericProxy({ merchantId, method, path, query, body, authMode = "app", scopes }) {
|
||||||
return callUberApi({
|
return callUberApi({
|
||||||
merchantId,
|
merchantId,
|
||||||
method,
|
method,
|
||||||
uberPath: path,
|
uberPath: path,
|
||||||
query,
|
query,
|
||||||
body,
|
body,
|
||||||
wrapperRoute: "/api/v1/uber/request"
|
wrapperRoute: "/api/v1/uber/request",
|
||||||
|
authMode,
|
||||||
|
scopes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +120,9 @@ async function menuUpsert({ merchantId, storeId, payload }) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
uberPath,
|
uberPath,
|
||||||
body: payload,
|
body: payload,
|
||||||
wrapperRoute: "/api/v1/uber/menu/upsert"
|
wrapperRoute: "/api/v1/uber/menu/upsert",
|
||||||
|
authMode: "app",
|
||||||
|
scopes: AUTH_SCOPES.STORE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +133,9 @@ async function menuReplace({ merchantId, storeId, payload }) {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
uberPath,
|
uberPath,
|
||||||
body: payload,
|
body: payload,
|
||||||
wrapperRoute: "/api/v1/uber/menu/replace"
|
wrapperRoute: "/api/v1/uber/menu/replace",
|
||||||
|
authMode: "app",
|
||||||
|
scopes: AUTH_SCOPES.STORE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +145,9 @@ async function menuGet({ merchantId, storeId }) {
|
|||||||
merchantId,
|
merchantId,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
uberPath,
|
uberPath,
|
||||||
wrapperRoute: "/api/v1/uber/menu"
|
wrapperRoute: "/api/v1/uber/menu",
|
||||||
|
authMode: "app",
|
||||||
|
scopes: AUTH_SCOPES.STORE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +158,9 @@ async function ordersList({ merchantId, storeId, query }) {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
uberPath,
|
uberPath,
|
||||||
query,
|
query,
|
||||||
wrapperRoute: "/api/v1/uber/orders"
|
wrapperRoute: "/api/v1/uber/orders",
|
||||||
|
authMode: "app",
|
||||||
|
scopes: AUTH_SCOPES.ORDER
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +184,9 @@ async function orderAction({ merchantId, orderId, action, payload }) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
uberPath,
|
uberPath,
|
||||||
body: payload || {},
|
body: payload || {},
|
||||||
wrapperRoute: "/api/v1/uber/orders/:orderId/action"
|
wrapperRoute: "/api/v1/uber/orders/:orderId/action",
|
||||||
|
authMode: "app",
|
||||||
|
scopes: AUTH_SCOPES.ORDER
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +197,9 @@ async function updateStoreHours({ merchantId, storeId, payload }) {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
uberPath,
|
uberPath,
|
||||||
body: payload,
|
body: payload,
|
||||||
wrapperRoute: "/api/v1/uber/stores/hours"
|
wrapperRoute: "/api/v1/uber/stores/hours",
|
||||||
|
authMode: "app",
|
||||||
|
scopes: AUTH_SCOPES.STORE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ router.get("/uber/callback", asyncHandler(controller.oauthCallback));
|
|||||||
* @openapi
|
* @openapi
|
||||||
* /api/v1/auth/uber/client-credentials-token:
|
* /api/v1/auth/uber/client-credentials-token:
|
||||||
* post:
|
* post:
|
||||||
* summary: Generate Uber client credentials token (sandbox/testing utility)
|
* summary: Get cached or live Uber client credentials token
|
||||||
* tags:
|
* tags:
|
||||||
* - Auth
|
* - Auth
|
||||||
* responses:
|
* responses:
|
||||||
@ -52,6 +52,19 @@ router.post(
|
|||||||
asyncHandler(controller.generateClientCredentialsToken)
|
asyncHandler(controller.generateClientCredentialsToken)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /api/v1/auth/uber/capabilities:
|
||||||
|
* get:
|
||||||
|
* summary: Get Uber auth grants, scopes, and token-rate-limit metadata
|
||||||
|
* tags:
|
||||||
|
* - Auth
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Auth capability details
|
||||||
|
*/
|
||||||
|
router.get("/uber/capabilities", asyncHandler(controller.getAuthCapabilities));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
* /api/v1/auth/uber/{merchantId}/refresh-token:
|
* /api/v1/auth/uber/{merchantId}/refresh-token:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user