Refactor route handlers and CMS clients

Refactored the route handlers to use a new `enhanceRouteHandler` function for better control over request handlers and user authentication. CMS clients are now created using factory functions for better encapsulation and control over instance creation. Renamed `client.ts` in 'keystatic' to `keystatic-client.ts`.
This commit is contained in:
giancarlo
2024-05-02 12:37:58 +07:00
parent be5c10f1c3
commit dbce7e38ae
8 changed files with 99 additions and 66 deletions

View File

@@ -1,4 +1,5 @@
import { getBillingEventHandlerService } from '@kit/billing-gateway'; import { getBillingEventHandlerService } from '@kit/billing-gateway';
import { enhanceRouteHandler } from '@kit/next/routes';
import { getLogger } from '@kit/shared/logger'; import { getLogger } from '@kit/shared/logger';
import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client'; import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client';
@@ -7,43 +8,42 @@ import billingConfig from '~/config/billing.config';
/** /**
* @description Handle the webhooks from Stripe related to checkouts * @description Handle the webhooks from Stripe related to checkouts
*/ */
export async function POST(request: Request) { export const POST = enhanceRouteHandler(
const provider = billingConfig.provider; async ({ request }) => {
const logger = await getLogger(); const provider = billingConfig.provider;
const logger = await getLogger();
const ctx = { const ctx = {
name: 'billing.webhook', name: 'billing.webhook',
provider, provider,
}; };
logger.info(ctx, `Received billing webhook. Processing...`); logger.info(ctx, `Received billing webhook. Processing...`);
const supabaseClientProvider = () => const supabaseClientProvider = () =>
getSupabaseRouteHandlerClient({ admin: true }); getSupabaseRouteHandlerClient({ admin: true });
const service = await getBillingEventHandlerService( const service = await getBillingEventHandlerService(
supabaseClientProvider, supabaseClientProvider,
provider, provider,
billingConfig, billingConfig,
);
try {
await service.handleWebhookEvent(request);
logger.info(ctx, `Successfully processed billing webhook`);
return new Response('OK', { status: 200 });
} catch (error) {
console.error(error);
logger.error(
{
...ctx,
error: JSON.stringify(error),
},
`Failed to process billing webhook`,
); );
return new Response('Error', { status: 500 }); try {
} await service.handleWebhookEvent(request);
}
logger.info(ctx, `Successfully processed billing webhook`);
return new Response('OK', { status: 200 });
} catch (error) {
logger.error(ctx, `Failed to process billing webhook`, error);
return new Response('Failed to process billing webhook', {
status: 500,
});
}
},
{
auth: false,
},
);

View File

@@ -1,18 +1,21 @@
import { getDatabaseWebhookHandlerService } from '@kit/database-webhooks'; import { getDatabaseWebhookHandlerService } from '@kit/database-webhooks';
import { enhanceRouteHandler } from '@kit/next/routes';
/** /**
* @name POST * @name POST
* @description POST handler for the webhook route that handles the webhook event * @description POST handler for the webhook route that handles the webhook event
*/ */
export async function POST(request: Request) { export const POST = enhanceRouteHandler(async ({ request }) => {
const service = getDatabaseWebhookHandlerService(); const service = getDatabaseWebhookHandlerService();
try { try {
// handle the webhook event // handle the webhook event
await service.handleWebhook(request); await service.handleWebhook(request);
// return a successful response
return new Response(null, { status: 200 }); return new Response(null, { status: 200 });
} catch { } catch (error) {
// return an error response
return new Response(null, { status: 500 }); return new Response(null, { status: 500 });
} }
} });

View File

@@ -45,7 +45,7 @@ export class StripeWebhookHandlerService
const stripe = await this.loadStripe(); const stripe = await this.loadStripe();
const event = stripe.webhooks.constructEvent( const event = await stripe.webhooks.constructEventAsync(
body, body,
signature, signature,
webhooksSecret, webhooksSecret,

View File

@@ -28,9 +28,11 @@ async function cmsClientFactory(type: CmsType) {
} }
async function getWordpressClient() { async function getWordpressClient() {
const { WordpressClient } = await import('../../wordpress/src/wp-client'); const { createWordpressClient } = await import(
'../../wordpress/src/wp-client'
);
return new WordpressClient(); return createWordpressClient();
} }
async function getKeystaticClient() { async function getKeystaticClient() {
@@ -38,9 +40,11 @@ async function getKeystaticClient() {
process.env.NEXT_RUNTIME === 'nodejs' || process.env.NEXT_RUNTIME === 'nodejs' ||
process.env.KEYSTATIC_STORAGE_KIND !== 'local' process.env.KEYSTATIC_STORAGE_KIND !== 'local'
) { ) {
const { KeystaticClient } = await import('../../keystatic/src/client'); const { createKeystaticClient } = await import(
'../../keystatic/src/keystatic-client'
);
return new KeystaticClient(); return createKeystaticClient();
} }
console.error( console.error(

View File

@@ -1,2 +1,2 @@
export * from './client'; export * from './keystatic-client';
export * from './content-renderer'; export * from './content-renderer';

View File

@@ -3,7 +3,11 @@ import { Cms, CmsClient } from '@kit/cms';
import { createKeystaticReader } from './create-reader'; import { createKeystaticReader } from './create-reader';
import { PostEntryProps } from './keystatic.config'; import { PostEntryProps } from './keystatic.config';
export class KeystaticClient implements CmsClient { export function createKeystaticClient() {
return new KeystaticClient();
}
class KeystaticClient implements CmsClient {
async getContentItems(options: Cms.GetContentItemsOptions) { async getContentItems(options: Cms.GetContentItemsOptions) {
const reader = await createKeystaticReader(); const reader = await createKeystaticReader();

View File

@@ -8,17 +8,19 @@ import { Cms, CmsClient } from '@kit/cms';
import GetTagsOptions = Cms.GetTagsOptions; import GetTagsOptions = Cms.GetTagsOptions;
export function createWordpressClient(
apiUrl = process.env.WORDPRESS_API_URL as string,
) {
return new WordpressClient(apiUrl);
}
/** /**
* @name WordpressClient * @name WordpressClient
* @description Represents a client for interacting with a Wordpress CMS. * @description Represents a client for interacting with a Wordpress CMS.
* Implements the CmsClient interface. * Implements the CmsClient interface.
*/ */
export class WordpressClient implements CmsClient { class WordpressClient implements CmsClient {
private readonly apiUrl: string; constructor(private readonly apiUrl: string) {}
constructor(apiUrl = process.env.WORDPRESS_API_URL as string) {
this.apiUrl = apiUrl;
}
/** /**
* Retrieves content items from a CMS based on the provided options. * Retrieves content items from a CMS based on the provided options.

View File

@@ -14,9 +14,19 @@ import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-clien
import { captureException, zodParseFactory } from '../utils'; import { captureException, zodParseFactory } from '../utils';
interface HandlerParams<Body> { interface Config<Schema> {
auth?: boolean;
captcha?: boolean;
captureException?: boolean;
schema?: Schema;
}
interface HandlerParams<
Body extends object,
RequireAuth extends boolean | undefined,
> {
request: NextRequest; request: NextRequest;
user: User; user: RequireAuth extends false ? undefined : User;
body: Body; body: Body;
} }
@@ -41,22 +51,21 @@ interface HandlerParams<Body> {
* *
*/ */
export const enhanceRouteHandler = < export const enhanceRouteHandler = <
Body, Body extends object,
Schema extends z.ZodType<Body, z.ZodTypeDef>, Schema extends z.ZodType<Body, z.ZodTypeDef>,
Params extends Config<Schema> = Config<Schema>,
>( >(
// Route handler function // Route handler function
handler: handler:
| ((params: HandlerParams<z.infer<Schema>>) => NextResponse | Response)
| (( | ((
params: HandlerParams<z.infer<Schema>>, params: HandlerParams<z.infer<Schema>, Params['auth']>,
) => NextResponse | Response)
| ((
params: HandlerParams<z.infer<Schema>, Params['auth']>,
) => Promise<NextResponse | Response>), ) => Promise<NextResponse | Response>),
// Parameters object // Parameters object
params?: { params?: Params,
captcha?: boolean;
captureException?: boolean;
schema?: Schema;
},
) => { ) => {
/** /**
* Route handler function. * Route handler function.
@@ -64,6 +73,10 @@ export const enhanceRouteHandler = <
* This function takes a request object as an argument and returns a response object. * This function takes a request object as an argument and returns a response object.
*/ */
return async function routeHandler(request: NextRequest) { return async function routeHandler(request: NextRequest) {
type UserParam = Params['auth'] extends false ? undefined : User;
let user: UserParam = undefined as UserParam;
// Check if the captcha token should be verified // Check if the captcha token should be verified
const shouldVerifyCaptcha = params?.captcha ?? false; const shouldVerifyCaptcha = params?.captcha ?? false;
@@ -80,15 +93,22 @@ export const enhanceRouteHandler = <
} }
const client = getSupabaseRouteHandlerClient(); const client = getSupabaseRouteHandlerClient();
const auth = await requireUser(client);
// If the user is not authenticated, redirect to the specified URL. const shouldVerifyAuth = params?.auth ?? true;
if (auth.error) {
return redirect(auth.redirectTo); // Check if the user should be authenticated
if (shouldVerifyAuth) {
// Get the authenticated user
const auth = await requireUser(client);
// If the user is not authenticated, redirect to the specified URL.
if (auth.error) {
return redirect(auth.redirectTo);
}
user = auth.data as UserParam;
} }
const user = auth.data;
// clone the request to read the body // clone the request to read the body
// so that we can pass it to the handler safely // so that we can pass it to the handler safely
let body = await request.clone().json(); let body = await request.clone().json();