Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions api-service/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,75 @@ paths:
- oauth2: [store:read]
x-swagger-router-controller: StoreManagement

/stores/api-usage-log:
get:
tags: [Store Management]
summary: Get API usage log
description: Retrieves a paginated list of detailed API usage logs for the authenticated store, with optional filtering.
operationId: getApiUsageLog
security:
- oauth2: [store:read] # Requires store read scope
parameters:
- in: query
name: keyId
schema:
type: string
required: false
description: Filter logs by a specific API key ID.
- in: query
name: startDate
schema:
type: string
format: date
required: false
description: Filter logs from this date (inclusive).
- in: query
name: endDate
schema:
type: string
format: date
required: false
description: Filter logs up to this date (inclusive).
- in: query
name: page
schema:
type: integer
default: 1
required: false
description: Page number for pagination.
- in: query
name: limit
schema:
type: integer
default: 15
required: false
description: Number of logs per page.
responses:
"200":
description: A paginated list of API usage logs.
content:
application/json:
schema:
type: object
properties:
logs:
type: array
items:
$ref: "#/components/schemas/ApiUsageLogEntry" # Define this schema below
pagination:
$ref: "#/components/schemas/PaginationInfo" # Define this schema below
"400":
$ref: "#/components/responses/BadRequestError"
"401":
$ref: "#/components/responses/UnauthorizedError"
"403":
$ref: "#/components/responses/ForbiddenError"
"404":
$ref: "#/components/responses/NotFoundError" # If store not found
"500":
$ref: "#/components/responses/InternalServerError"
x-swagger-router-controller: StoreManagement # Route to StoreManagement controller

/health:
get:
tags: [Health]
Expand Down Expand Up @@ -1581,6 +1650,51 @@ components:
items:
$ref: "#/components/schemas/MonthlySpendingItem"

ApiUsageLogEntry:
type: object
properties:
_id:
type: string
description: The unique ID of the log entry.
storeId:
type: string
description: The ID of the store associated with the API key.
apiKeyId:
type: string
description: The ID of the API key used.
apiKeyPrefix:
type: string
description: The prefix of the API key used.
endpoint:
type: string
description: The API endpoint accessed.
method:
type: string
description: The HTTP method used.
timestamp:
type: string
format: date-time
description: The timestamp when the request occurred.
userAgent:
type: string
description: The user agent of the client making the request.

PaginationInfo:
type: object
properties:
currentPage:
type: integer
description: The current page number.
totalPages:
type: integer
description: The total number of pages available.
totalItems:
type: integer
description: The total number of items matching the query.
limit:
type: integer
description: The number of items per page.

responses:
BadRequestError:
description: Bad request - invalid input
Expand Down
12 changes: 11 additions & 1 deletion api-service/controllers/StoreManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,14 @@ module.exports.getApiKeyUsage = function getApiKeyUsage(req, res, next, keyId) {
.catch((response) => {
utils.writeJson(res, response);
});
}
};

module.exports.getApiUsageLog = function getApiUsageLog(req, res, next) {
StoreManagement.getApiUsageLog(req)
.then((response) => {
utils.writeJson(res, response);
})
.catch((response) => {
utils.writeJson(res, response);
});
};
86 changes: 86 additions & 0 deletions api-service/service/StoreManagementService.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,90 @@ exports.getApiKeyUsage = async function (req, keyId) {
console.error('Get API key usage failed:', error);
return respondWithCode(500, { code: 500, message: 'Internal server error' });
}
};

/**
* Get API Usage Log
* Get paginated detailed usage logs for the store's API keys
*/
exports.getApiUsageLog = async function (req) {
try {
const db = getDB();

// Get user data (store owner)
const userData = req.user || await getUserData(req.headers.authorization?.split(' ')[1]);

// Find the store to ensure it exists and get its ID
const store = await db.collection('stores').findOne(
{ auth0Id: userData.sub },
{ projection: { _id: 1 } } // Only need the store's _id
);
if (!store) {
return respondWithCode(404, { code: 404, message: 'Store not found' });
}
const storeId = store._id.toString();

// Get query parameters
const { keyId, startDate, endDate } = req.query;
const page = parseInt(req.query.page || '1', 10);
const limit = parseInt(req.query.limit || '15', 10);
const skip = (page - 1) * limit;

// Build the query filter
const filter = { storeId: storeId }; // Filter by the authenticated store

if (keyId) {
// Optional: Validate if keyId belongs to this store?
// For now, just filter by it if provided.
filter.apiKeyId = keyId;
}

const dateFilter = {};
if (startDate) {
try {
dateFilter.$gte = new Date(startDate);
} catch (e) {
return respondWithCode(400, { code: 400, message: 'Invalid startDate format' });
}
}
if (endDate) {
try {
// Add 1 day to endDate to make it inclusive of the whole day
const end = new Date(endDate);
end.setDate(end.getDate() + 1);
dateFilter.$lt = end; // Use $lt with the next day
} catch (e) {
return respondWithCode(400, { code: 400, message: 'Invalid endDate format' });
}
}
if (Object.keys(dateFilter).length > 0) {
filter.timestamp = dateFilter;
}

// Get total count for pagination
const totalItems = await db.collection('apiUsage').countDocuments(filter);
const totalPages = Math.ceil(totalItems / limit);

// Get paginated logs, sorted by timestamp descending
const logs = await db.collection('apiUsage')
.find(filter)
.sort({ timestamp: -1 }) // Show most recent first
.skip(skip)
.limit(limit)
.toArray();

return respondWithCode(200, {
logs,
pagination: {
currentPage: page,
totalPages,
totalItems,
limit,
},
});

} catch (error) {
console.error('Get API usage log failed:', error);
return respondWithCode(500, { code: 500, message: 'Internal server error' });
}
};
63 changes: 52 additions & 11 deletions web/src/api/hooks/useStoreHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ import {
ApiKeyCreate,
StoreUpdate,
StoreBasicInfo,
Error, // <-- Import Error type
SearchStoresParams, // <-- Import SearchStoresParams if generated
Error,
SearchStoresParams,
GetApiKeyUsagePayload, // <-- Import payload type for usage stats
GetApiUsageLogParams, // <-- Import params type for usage log
ApiUsageLogEntry, // <-- Import log entry type
PaginationInfo, // <-- Import pagination info type
} from "../types/data-contracts";
import { useAuth } from "../../hooks/useAuth";
import { useState, useEffect } from "react"; // <-- Import useState and useEffect for debounce
import { useState, useEffect } from "react";

// Define the expected response structure for getApiUsageLog
interface ApiUsageLogResponse {
logs?: ApiUsageLogEntry[];
pagination?: PaginationInfo;
}

export function useStoreProfile() {
// Get clientsReady state
Expand Down Expand Up @@ -78,22 +88,53 @@ export function useRevokeApiKey() {
});
}

export function useApiKeyUsage(keyId: string) {
// Get clientsReady state
// Update useApiKeyUsage to accept payload
export function useApiKeyUsage(
keyId: string,
payload?: GetApiKeyUsagePayload, // Accept payload
) {
const { apiClients, clientsReady } = useApiClients();
const { isAuthenticated, isLoading: authLoading } = useAuth(); // Get auth state
const { isAuthenticated, isLoading: authLoading } = useAuth();

// Include payload in the query key if present
// This object now matches the expected parameter type for the updated cache key
const queryKeyParams = { keyId, ...payload };

return useQuery({
queryKey: cacheKeys.stores.apiKeyUsage(keyId),
// This call should now be valid
queryKey: cacheKeys.stores.apiKeyUsage(queryKeyParams),
queryFn: () =>
apiClients.stores.getApiKeyUsage(keyId).then((res) => res.data),
// Update enabled check, keeping !!keyId
// Pass payload to the API call
apiClients.stores.getApiKeyUsage(keyId, payload).then((res) => res.data),
enabled: !!keyId && isAuthenticated && !authLoading && clientsReady,
...cacheSettings.apiKeys,
...cacheSettings.apiKeys, // Consider specific cache settings for usage
});
}

// --- New Hook for API Usage Log ---
export function useApiUsageLog(params?: GetApiUsageLogParams) {
const { apiClients, clientsReady } = useApiClients();
const { isAuthenticated, isLoading: authLoading } = useAuth();

// Ensure params is always an object for the query key
const queryParams = params || {};

return useQuery<ApiUsageLogResponse, Error>({
// Use the defined response type
queryKey: cacheKeys.stores.apiUsageLog(queryParams), // Use the new cache key
queryFn: () =>
// Pass the queryParams object to the API client method
apiClients.stores.getApiUsageLog(queryParams).then((res) => res.data),
// Enable only when authenticated and client is ready
enabled: isAuthenticated && !authLoading && clientsReady,
// Keep previous data while fetching new page/filters
placeholderData: (previousData) => previousData,
// Consider specific cache settings for logs if needed
// staleTime: CACHE_TIMES.SHORT,
});
}
// --- End New Hook ---

// --- New Hook for Searching Stores ---
export function useSearchStores(searchTerm: string, debounceMs = 300) {
const { apiClients, clientsReady } = useApiClients();
const { isAuthenticated, isLoading: authLoading } = useAuth();
Expand Down
37 changes: 37 additions & 0 deletions web/src/api/types/Stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import {
ApiKeyCreate,
ApiKeyList,
ApiKeyUsage,
ApiUsageLogEntry,
Error,
GetApiKeyUsagePayload,
GetApiUsageLogParams,
LookupStoresParams,
PaginationInfo,
SearchStoresParams,
Store,
StoreBasicInfo,
Expand Down Expand Up @@ -209,6 +212,40 @@ export class Stores<
...params,
});
/**
* @description Retrieves a paginated list of detailed API usage logs for the authenticated store, with optional filtering.
*
* @tags Store Management
* @name GetApiUsageLog
* @summary Get API usage log
* @request GET:/stores/api-usage-log
* @secure
* @response `200` `{
logs?: (ApiUsageLogEntry)[],
pagination?: PaginationInfo,

}` A paginated list of API usage logs.
* @response `400` `Error`
* @response `401` `Error`
* @response `403` `Error`
* @response `404` `Error`
* @response `500` `Error`
*/
getApiUsageLog = (query: GetApiUsageLogParams, params: RequestParams = {}) =>
this.request<
{
logs?: ApiUsageLogEntry[];
pagination?: PaginationInfo;
},
Error
>({
path: `/stores/api-usage-log`,
method: "GET",
query: query,
secure: true,
format: "json",
...params,
});
/**
* @description Retrieves basic details (like name) for a list of store IDs.
*
* @tags Store Management
Expand Down
Loading
Loading