Skip to content

Commit

Permalink
refactor(extension): begin replacing JWTs with salted hashes
Browse files Browse the repository at this point in the history
  • Loading branch information
david-r-cox committed Sep 26, 2024
1 parent fc62c2b commit 1d96ccd
Show file tree
Hide file tree
Showing 11 changed files with 1,142 additions and 3,268 deletions.
2 changes: 1 addition & 1 deletion client/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ apply-integration-test-migrations:
@echo "ALTER TABLE public.test_accounts ENABLE ROW LEVEL SECURITY;" >> create_test_accounts.sql
@echo "" >> create_test_accounts.sql
@echo "CREATE POLICY \"Users can access their own test account\" ON public.test_accounts TO anon, authenticated" >> create_test_accounts.sql
@echo " USING ((COALESCE(auth.uid (), keyhippo.key_uid ()) = user_id));" >> create_test_accounts.sql
@echo " USING ((COALESCE(auth.uid (), keyhippo.current_user_id ()) = user_id));" >> create_test_accounts.sql
@echo "" >> create_test_accounts.sql
@echo "GRANT SELECT ON public.test_accounts TO anon, authenticated;" >> create_test_accounts.sql
@echo "GRANT INSERT, UPDATE, DELETE ON public.test_accounts TO authenticated;" >> create_test_accounts.sql
Expand Down
125 changes: 63 additions & 62 deletions client/src/api-keys/createApiKey.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
import { SupabaseClient } from "@supabase/supabase-js";
import { v4 as uuidv4 } from "uuid";
import { CompleteApiKeyInfo, Logger } from "../types";
import { logInfo, logError, createDatabaseError } from "../utils";
import {
ApiKeyEntity,
Logger,
ApplicationError,
ApiKeyText,
ApiKeyId,
UserId,
Description,
Timestamp,
} from "../types";
import { createDatabaseError } from "../utils";

/**
* Generates a unique description by appending a UUID to the provided key description.
* @param keyDescription - The base description for the API key.
* @returns A unique description string combining a UUID and the provided key description.
* Interface for the result of the create API key RPC call.
*/
const generateUniqueDescription = (keyDescription: string): string => {
const uniqueId = uuidv4();
return `${uniqueId}-${keyDescription}`;
};
interface ApiKeyRpcResult {
api_key: ApiKeyText;
api_key_id: ApiKeyId;
}

/**
* Executes the RPC call to create a new API key in the database.
* @param supabase - The Supabase client instance.
* @param userId - The ID of the user for whom the API key is being created.
* @param uniqueDescription - A unique description for the new API key.
* @param keyDescription - A description for the new API key.
* @returns A promise that resolves with the created API key and its ID.
* @throws Error if the RPC call fails to create the API key.
*/
const executeCreateApiKeyRpc = async (
supabase: SupabaseClient<any, "public", any>,
userId: string,
uniqueDescription: string,
): Promise<{ apiKey: string; apiKeyId: string }> => {
keyDescription: Description,
): Promise<{ apiKey: ApiKeyText; apiKeyId: ApiKeyId }> => {
const { data, error } = await supabase
.schema("keyhippo")
.rpc("create_api_key", {
id_of_user: userId,
key_description: uniqueDescription,
})
.single<{ api_key: string; api_key_id: string }>();
.rpc("create_api_key", { key_description: keyDescription })
.single<ApiKeyRpcResult>();

if (error) {
throw new Error(`Create API key RPC failed: ${error.message}`);
Expand All @@ -46,84 +47,84 @@ const executeCreateApiKeyRpc = async (
};

/**
* Constructs a CompleteApiKeyInfo object from the provided details.
* @param id - The unique identifier of the API key.
* @param description - The description of the API key.
* @param apiKey - The actual API key string.
* @returns A CompleteApiKeyInfo object containing all relevant API key details.
* Fetches the full API key metadata from the database.
* @param supabase - The Supabase client instance.
* @param apiKeyId - The unique identifier of the API key.
* @returns A promise that resolves with the full API key metadata.
* @throws Error if the query fails to retrieve the API key metadata.
*/
const buildCompleteApiKeyInfo = (
id: string,
description: string,
apiKey: string,
): CompleteApiKeyInfo => ({
id,
description,
apiKey,
status: "success",
});
const fetchApiKeyMetadata = async (
supabase: SupabaseClient<any, "public", any>,
apiKeyId: ApiKeyId,
): Promise<Omit<ApiKeyEntity, "apiKey">> => {
const { data, error } = await supabase
.schema("keyhippo")
.from("api_key_metadata")
.select("*")
.eq("id", apiKeyId)
.single<Omit<ApiKeyEntity, "apiKey">>();

if (error) {
throw new Error(`Failed to fetch API key metadata: ${error.message}`);
}

if (!data) {
throw new Error("API key metadata not found");
}

return data;
};

/**
* Logs the successful creation of a new API key for a user.
* Logs the successful creation of a new API key.
* @param logger - The logger instance used for logging.
* @param userId - The ID of the user for whom the API key was created.
* @param keyId - The unique identifier of the newly created API key.
*/
const logApiKeyCreation = (
logger: Logger,
userId: string,
keyId: string,
keyId: ApiKeyId,
): void => {
logInfo(logger, `New API key created for user: ${userId}, Key ID: ${keyId}`);
logger.info(`New API key created with Key ID: ${keyId}`);
};

/**
* Handles errors that occur during the API key creation process.
* @param error - The error encountered during the process.
* @param logger - The logger instance used for logging errors.
* @throws AppError encapsulating the original error with a descriptive message.
* @throws ApplicationError encapsulating the original error with a descriptive message.
*/
const handleCreateApiKeyError = (error: unknown, logger: Logger): never => {
logError(logger, `Failed to create new API key: ${error}`);
logger.error(`Failed to create new API key: ${error}`);
throw createDatabaseError(`Failed to create API key: ${error}`);
};

/**
* Creates a new API key for a user.
* @param supabase - The Supabase client.
* @param userId - The ID of the user.
* @param keyDescription - Description of the API key.
* @param logger - The logger instance.
* @returns The complete information of the created API key.
* @throws AppError if the creation process fails.
* @returns The comprehensive information of the created API key.
* @throws ApplicationError if the creation process fails.
*/
export const createApiKey = async (
supabase: SupabaseClient<any, "public", any>,
userId: string,
keyDescription: string,
keyDescription: Description,
logger: Logger,
): Promise<CompleteApiKeyInfo> => {
const uniqueDescription = generateUniqueDescription(keyDescription);

): Promise<ApiKeyEntity> => {
try {
// Call the RPC to create the API key and get the result
const { apiKey, apiKeyId } = await executeCreateApiKeyRpc(
supabase,
userId,
uniqueDescription,
keyDescription,
);
const apiKeyMetadata = await fetchApiKeyMetadata(supabase, apiKeyId);

// Build the complete API key information object
const completeKeyInfo: CompleteApiKeyInfo = buildCompleteApiKeyInfo(
apiKeyId,
uniqueDescription,
const apiKeyEntity: ApiKeyEntity = {
...apiKeyMetadata,
apiKey,
);

// Log the successful API key creation
logApiKeyCreation(logger, userId, completeKeyInfo.id);
};

return completeKeyInfo;
logApiKeyCreation(logger, apiKeyEntity.id);
return apiKeyEntity;
} catch (error) {
return handleCreateApiKeyError(error, logger);
}
Expand Down
109 changes: 57 additions & 52 deletions client/src/api-keys/loadApiKeyInfo.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,99 @@
import { SupabaseClient } from "@supabase/supabase-js";
import { ApiKeyInfo, Logger } from "../types";
import {
logDebug,
logWarn,
logInfo,
logError,
createDatabaseError,
parseApiKeyInfo,
validateRpcResult
} from "../utils";
import { ApiKeySummary, Logger } from "../types";
import { logDebug, logInfo, logError, createDatabaseError } from "../utils";

// Define the shape of the data returned from the query
interface ApiKeyMetadata {
id: string;
description: string;
}

/**
* Executes the RPC call to load API key information for a user.
* Executes the query to load API key summary information for the current user.
* @param supabase - The Supabase client instance.
* @param userId - The ID of the user whose API key information is being loaded.
* @returns A promise that resolves with the RPC result containing data or an error.
* @throws Error if the RPC call fails.
* @returns A promise that resolves with the query result containing data or an error.
* @throws Error if the query fails.
*/
const executeLoadApiKeyInfoRpc = async (
const executeLoadApiKeySummaryQuery = async (
supabase: SupabaseClient<any, "public", any>,
userId: string,
): Promise<any> => {
): Promise<{ data: ApiKeyMetadata[] | null; error: any }> => {
return await supabase
.schema("keyhippo")
.rpc("load_api_key_info", { id_of_user: userId });
.from("api_key_metadata")
.select("id, description")
.eq("is_revoked", false);
};

/**
* Logs detailed information about the RPC call result.
* Logs detailed information about the query result.
* @param logger - The logger instance used for logging.
* @param result - The result object returned from the RPC call.
* @param result - The result object returned from the query.
*/
const logRpcResult = (logger: Logger, result: any): void => {
logDebug(logger, `Raw result from RPC: ${JSON.stringify(result)}`);
logDebug(
logger,
`Result status: ${result.status}, statusText: ${result.statusText}`,
);
const logQueryResult = (
logger: Logger,
result: { data: ApiKeyMetadata[] | null; error: any },
): void => {
logDebug(logger, `Raw result from query: ${JSON.stringify(result)}`);
logDebug(logger, `Result error: ${JSON.stringify(result.error)}`);
logDebug(logger, `Result data: ${JSON.stringify(result.data)}`);
};

/**
* Logs the successful loading of API key information.
* Logs the successful loading of API key summary information.
* @param logger - The logger instance used for logging.
* @param userId - The ID of the user whose API key information was loaded.
* @param count - The number of API key information entries loaded.
* @param count - The number of API key summary entries loaded.
*/
const logApiKeyInfoLoaded = (
logger: Logger,
userId: string,
count: number,
): void => {
logInfo(logger, `API key info loaded for user: ${userId}. Count: ${count}`);
const logApiKeySummaryLoaded = (logger: Logger, count: number): void => {
logInfo(logger, `API key summaries loaded. Count: ${count}`);
};

/**
* Handles errors that occur during the loading of API key information.
* Handles errors that occur during the loading of API key summary information.
* @param error - The error encountered during the loading process.
* @param logger - The logger instance used for logging errors.
* @throws AppError encapsulating the original error with a descriptive message.
*/
const handleLoadApiKeyInfoError = (error: unknown, logger: Logger): never => {
logError(logger, `Failed to load API key info: ${error}`);
throw createDatabaseError(`Failed to load API key info: ${error}`);
const handleLoadApiKeySummaryError = (
error: unknown,
logger: Logger,
): never => {
logError(logger, `Failed to load API key summaries: ${error}`);
throw createDatabaseError(`Failed to load API key summaries: ${error}`);
};

/**
* Loads API key information for a user.
* Loads API key summary information for the current user.
* @param supabase - The Supabase client used to interact with the database.
* @param userId - The ID of the user whose API key information is to be loaded.
* @param logger - The logger instance used for logging events and errors.
* @returns A promise that resolves with an array of API key information.
* @returns A promise that resolves with an array of API key summary information.
* @throws AppError if the loading process fails.
*/
export const loadApiKeyInfo = async (
export const loadApiKeySummaries = async (
supabase: SupabaseClient<any, "public", any>,
userId: string,
logger: Logger,
): Promise<ApiKeyInfo[]> => {
): Promise<ApiKeySummary[]> => {
try {
const result = await executeLoadApiKeyInfoRpc(supabase, userId);
logRpcResult(logger, result);
validateRpcResult(result, "load_api_key_info");
const { data, error } = await executeLoadApiKeySummaryQuery(supabase);
logQueryResult(logger, { data, error });

if (error) {
throw error;
}

if (!data) {
return [];
}

const apiKeyInfo = parseApiKeyInfo(result.data);
logApiKeyInfoLoaded(logger, userId, apiKeyInfo.length);
const apiKeySummaries: ApiKeySummary[] = data.map(
(item: ApiKeyMetadata) => ({
id: item.id,
description: item.description,
}),
);

return apiKeyInfo;
logApiKeySummaryLoaded(logger, apiKeySummaries.length);
return apiKeySummaries;
} catch (error) {
return handleLoadApiKeyInfoError(error, logger);
return handleLoadApiKeySummaryError(error, logger);
}
};
Loading

0 comments on commit 1d96ccd

Please sign in to comment.