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
47 changes: 30 additions & 17 deletions tapiro-api-external/middleware/apiKeyMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,38 @@ const validateApiKey = async (req, scopes, schema) => {
throw new Error('API key required');
}

const apiKeyDetailsCacheKey = `${CACHE_KEYS.API_KEY_DETAILS}${apiKey}`; // New cache key
const apiKeyDetailsCacheKey = `${CACHE_KEYS.API_KEY_DETAILS}${apiKey}`;
let cachedApiKeyDetails = await getCache(apiKeyDetailsCacheKey);

if (cachedApiKeyDetails) {
cachedApiKeyDetails = JSON.parse(cachedApiKeyDetails);
if (cachedApiKeyDetails.status === 'active') {
req.storeId = cachedApiKeyDetails.storeId;
req.keyId = cachedApiKeyDetails.keyId; // Set keyId for tracking
const { keyId: cachedKeyId, storeId: cachedStoreId, status: cachedStatus } = cachedApiKeyDetails;

if (cachedStatus === 'active') {
// Check for an explicit revocation marker for this keyId
let isMarkedRevoked = false;
if (cachedKeyId) {
const markerKey = `revoked_api_key_marker:${cachedKeyId}`;
const revocationMarker = await getCache(markerKey);
if (revocationMarker === 'revoked') {
isMarkedRevoked = true;
}
}

if (isMarkedRevoked) {
console.log(`API key ${apiKey.substring(0,8)}... (keyId: ${cachedKeyId}) found revocation marker. Invalidating local cache.`);
await invalidateCache(apiKeyDetailsCacheKey); // Invalidate this specific raw API key's cache
throw new Error('API key revoked or invalid (marker)');
}

// If not marked revoked, proceed
req.storeId = cachedStoreId;
req.keyId = cachedKeyId;
trackApiUsage(req, apiKey, req.storeId, req.keyId);
return true;
} else {
// Key was cached but is not active (e.g. revoked)
throw new Error('API key revoked or invalid');
// Key was cached but is not active (e.g. revoked directly in this cache)
throw new Error('API key revoked or invalid (cached as non-active)');
}
}

Expand All @@ -63,7 +82,7 @@ const validateApiKey = async (req, scopes, schema) => {

const store = await db.collection('stores').findOne({
'apiKeys.prefix': prefix,
'apiKeys.status': 'active', // Query for active keys directly
'apiKeys.status': 'active',
});

if (!store) {
Expand All @@ -75,8 +94,6 @@ const validateApiKey = async (req, scopes, schema) => {
);

if (!foundKey) {
// This case should ideally be covered by the store query if prefix is unique enough
// and status is checked.
throw new Error('Invalid API key (specific key not found or inactive)');
}

Expand All @@ -86,26 +103,22 @@ const validateApiKey = async (req, scopes, schema) => {
throw new Error('Invalid API key (hash mismatch)');
}

// Set store ID and key ID in request
req.storeId = store._id.toString();
req.keyId = foundKey.keyId;

// Cache the API key details (storeId, keyId, status)
const apiKeyDetailsToCache = {
storeId: req.storeId,
keyId: req.keyId,
status: foundKey.status, // Should be 'active' here
status: foundKey.status,
};
await setCache(apiKeyDetailsCacheKey, JSON.stringify(apiKeyDetailsToCache), { EX: CACHE_TTL.API_KEY || 1800 });
// Use the correct TTL from local cacheConfig for API_KEY_DETAILS
await setCache(apiKeyDetailsCacheKey, JSON.stringify(apiKeyDetailsToCache), { EX: CACHE_TTL.API_KEY_DETAILS || 1800 });

trackApiUsage(req, apiKey, req.storeId, req.keyId);

return true;
} catch (error) {
console.error('API key validation failed:', error.message); // Log only message for brevity
// Re-throw to be handled by the oas3-tools error handler or a global error handler
// which should return a proper HTTP error response.
// Avoid directly sending res.status here as it bypasses standard error flow.
console.error('API key validation failed:', error.message);
throw error;
}
};
Expand Down
27 changes: 22 additions & 5 deletions tapiro-api-external/service/StoreOperationsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ exports.getUserPreferences = async function (req, userId) {
// Try cache first using preference-specific cache key with constants
const cacheKey = `${CACHE_KEYS.STORE_PREFERENCES}${userId}:${req.storeId}`;
const cachedPrefs = await getCache(cacheKey);
if (cachedPrefs) {
return respondWithCode(200, JSON.parse(cachedPrefs));
}

// Find user in database by email only

// Find user in database by email only (needed regardless of cache hit/miss for logging)
const user = await db.collection('users').findOne({ email: userId });

if (!user) {
Expand All @@ -54,6 +51,26 @@ exports.getUserPreferences = async function (req, userId) {
});
}

// Log preference access event regardless of cache hit/miss
await db.collection('userData').insertOne({
userId: user._id,
storeId: req.storeId,
email: userId,
dataType: 'preference_access', // New data type for preference access
entries: [{ timestamp: new Date() }], // Simplified entry structure for preference access
metadata: {
source: req.keyId ? `api-key:${req.keyId}` : 'api',
userAgent: req.headers['user-agent'] || 'unknown',
},
processedStatus: 'processed', // No need for AI processing for preference access
timestamp: new Date(),
});

// Return cached response if available
if (cachedPrefs) {
return respondWithCode(200, JSON.parse(cachedPrefs));
}

// Check if user has explicitly opted in to this store
const isOptedIn = user.privacySettings?.optInStores?.includes(req.storeId);

Expand Down
1 change: 1 addition & 0 deletions tapiro-api-external/utils/cacheConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const CACHE_TTL = {
USER_DATA: 3600, // User profiles - 1 hour
STORE_DATA: 3600, // Store profiles - 1 hour
API_KEY: 1800, // API keys - 30 minutes
API_KEY_DETAILS: 1800, // TTL for detailed API key info - 30 minutes
INVALIDATION: 1, // Short TTL for invalidation
AI_REQUEST: 60, // AI service requests - 1 minute
};
Expand Down
20 changes: 16 additions & 4 deletions tapiro-api-internal/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -641,11 +641,11 @@ paths:
default: 1
- name: dataType
in: query
description: Filter by data type (purchase or search)
description: Filter by data type (purchase, search, or preference_access)
required: false
schema:
type: string
enum: [purchase, search]
enum: [purchase, search, preference_access]
- name: storeId
in: query
description: Filter by store ID
Expand Down Expand Up @@ -1596,6 +1596,17 @@ components:
items:
type: string

PreferenceAccessEntry:
type: object
required:
- timestamp
properties:
timestamp:
type: string
format: date-time
description: ISO 8601 timestamp of when the preferences were accessed.
description: Record of when a store accessed a user's preference data.

RecentUserDataEntry:
type: object
properties:
Expand All @@ -1607,7 +1618,7 @@ components:
description: The ID of the store that submitted the data.
dataType:
type: string
enum: [purchase, search]
enum: [purchase, search, preference_access]
description: The type of data submitted.
timestamp:
type: string
Expand All @@ -1619,11 +1630,12 @@ components:
description: The timestamp of the original event (e.g., purchase time).
details:
type: array
description: Array of individual purchase or search events within this batch.
description: Array of individual purchase, search, or preference access events within this batch.
items:
oneOf:
- $ref: "#/components/schemas/PurchaseEntry"
- $ref: "#/components/schemas/SearchEntry"
- $ref: "#/components/schemas/PreferenceAccessEntry"

StoreBasicInfo:
type: object
Expand Down
24 changes: 16 additions & 8 deletions tapiro-api-internal/service/StoreManagementService.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const crypto = require('crypto');
const { ObjectId } = require('mongodb');
const { getDB } = require('../utils/mongoUtil');
const { respondWithCode } = require('../utils/writer');
const { setCache, getCache, invalidateCache } = require('../utils/redisUtil');
const { respondWithCode } = require('../utils/writer');
const { getUserData } = require('../utils/authUtil');
const { CACHE_TTL, CACHE_KEYS } = require('../utils/cacheConfig');
const { ObjectId } = require('mongodb');
const crypto = require('crypto');

/**
* Create API Key
Expand Down Expand Up @@ -145,9 +145,7 @@ exports.getApiKeys = async function (req) {
exports.revokeApiKey = async function (req, keyId) {
try {
const db = getDB();

// Get user data - use req.user if available (from middleware) or fetch it
const userData = req.user || await getUserData(req.headers.authorization?.split(' ')[1]);
const userData = req.user || (await getUserData(req.headers.authorization?.split(' ')[1]));

// Get the store with API keys first to find the prefix of the key being revoked
const store = await db.collection('stores').findOne({ auth0Id: userData.sub });
Expand Down Expand Up @@ -191,12 +189,22 @@ exports.revokeApiKey = async function (req, keyId) {
if (result.matchedCount === 0) {
return respondWithCode(404, {
code: 404,
message: 'Store not found',
message: 'Store not found or API key not matched during update',
});
}

// Invalidate store cache to reflect modified API key
// Invalidate store cache to reflect modified API key for the store owner's view
await invalidateCache(`${CACHE_KEYS.STORE_DATA}${userData.sub}`);

// Set a revocation marker for the specific keyId.
// The external API will check for this marker.
const markerKey = `revoked_api_key_marker:${keyToRevoke.keyId}`;
const externalApiKeyCacheTTL = CACHE_TTL.EXTERNAL_API_KEY_DETAILS || 1800; // Use the new constant
const markerBuffer = 300; // 5 minutes buffer
const markerTTL = externalApiKeyCacheTTL + markerBuffer;

await setCache(markerKey, 'revoked', { EX: markerTTL });
console.log(`Set revocation marker for keyId ${keyToRevoke.keyId} with TTL ${markerTTL}s. Marker key: ${markerKey}`);

return respondWithCode(204);
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions tapiro-api-internal/utils/cacheConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const CACHE_TTL = {
INVALIDATION: 1, // Short TTL for invalidation
AI_REQUEST: 60, // AI service requests - 1 minute
TAXONOMY: 86400, // Taxonomy data - 1 day (added)
EXTERNAL_API_KEY_DETAILS: 1800, // TTL for API key details in the external API - 30 minutes
};

/**
Expand Down
6 changes: 3 additions & 3 deletions tapiro-api-internal/utils/dbSchemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

// Schema version tracking
const SCHEMA_VERSION = '3.0.0'; // Incremented version
const SCHEMA_VERSION = '3.0.1'; // Incremented version

const userSchema = {
validator: {
Expand Down Expand Up @@ -255,8 +255,8 @@ const userDataSchema = {
email: { bsonType: 'string' },
dataType: {
bsonType: 'string',
enum: ['purchase', 'search'],
description: 'Type of data being stored',
enum: ['purchase', 'search', 'preference_access'],
description: 'Type of data collected',
},
entries: {
bsonType: 'array',
Expand Down
2 changes: 1 addition & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flowbite React Template React Router - Data mode</title>
<title>Tapiro: Centralized Data Management</title>
</head>
<body>
<div id="root"></div>
Expand Down
19 changes: 14 additions & 5 deletions web/src/api/types/data-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,22 @@ export interface SearchEntry {
clicked?: string[] | null;
}

/** Record of when a store accessed a user's preference data. */
export interface PreferenceAccessEntry {
/**
* ISO 8601 timestamp of when the preferences were accessed.
* @format date-time
*/
timestamp: string;
}

export interface RecentUserDataEntry {
/** The unique ID of the userData entry. */
_id?: string;
/** The ID of the store that submitted the data. */
storeId?: string;
/** The type of data submitted. */
dataType?: "purchase" | "search";
dataType?: "purchase" | "search" | "preference_access";
/**
* When the data was submitted to Tapiro.
* @format date-time
Expand All @@ -501,8 +510,8 @@ export interface RecentUserDataEntry {
* @format date-time
*/
entryTimestamp?: string;
/** Array of individual purchase or search events within this batch. */
details?: (PurchaseEntry | SearchEntry)[];
/** Array of individual purchase, search, or preference access events within this batch. */
details?: (PurchaseEntry | SearchEntry | PreferenceAccessEntry)[];
}

export interface StoreBasicInfo {
Expand Down Expand Up @@ -605,8 +614,8 @@ export interface GetRecentUserDataParams {
* @default 1
*/
page?: number;
/** Filter by data type (purchase or search) */
dataType?: "purchase" | "search";
/** Filter by data type (purchase, search, or preference_access) */
dataType?: "purchase" | "search" | "preference_access";
/** Filter by store ID */
storeId?: string;
/**
Expand Down
Loading
Loading