B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

View File

@@ -0,0 +1,76 @@
import 'server-only';
import { redirect } from 'next/navigation';
import type { User } from '@supabase/supabase-js';
import { ZodType, z } from 'zod';
import { verifyCaptchaToken } from '@kit/auth/captcha/server';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { zodParseFactory } from '../utils';
/**
* @name enhanceAction
* @description Enhance an action with captcha, schema and auth checks
*/
export function enhanceAction<
Args,
Response,
Config extends {
auth?: boolean;
captcha?: boolean;
schema?: z.ZodType<
Config['captcha'] extends true ? Args & { captchaToken: string } : Args,
z.ZodTypeDef
>;
},
>(
fn: (
params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
user: Config['auth'] extends false ? undefined : User,
) => Response | Promise<Response>,
config: Config,
) {
return async (
params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
) => {
type UserParam = Config['auth'] extends false ? undefined : User;
const requireAuth = config.auth ?? true;
let user: UserParam = undefined as UserParam;
// validate the schema passed in the config if it exists
const data = config.schema
? zodParseFactory(config.schema)(params)
: params;
// by default, the CAPTCHA token is not required
const verifyCaptcha = config.captcha ?? false;
// verify the CAPTCHA token. It will throw an error if the token is invalid.
if (verifyCaptcha) {
const token = (data as Args & { captchaToken: string }).captchaToken;
// Verify the CAPTCHA token. It will throw an error if the token is invalid.
await verifyCaptchaToken(token);
}
// verify the user is authenticated if required
if (requireAuth) {
// verify the user is authenticated if required
const auth = await requireUser(getSupabaseServerClient());
// If the user is not authenticated, redirect to the specified URL.
if (!auth.data) {
redirect(auth.redirectTo);
}
user = auth.data as UserParam;
}
return fn(data, user);
};
}

View File

@@ -0,0 +1,140 @@
import 'server-only';
import { redirect } from 'next/navigation';
import { NextRequest, NextResponse } from 'next/server';
import { User } from '@supabase/supabase-js';
import { z } from 'zod';
import { verifyCaptchaToken } from '@kit/auth/captcha/server';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { zodParseFactory } from '../utils';
interface Config<Schema> {
auth?: boolean;
captcha?: boolean;
schema?: Schema;
}
interface HandlerParams<
Schema extends z.ZodType | undefined,
RequireAuth extends boolean | undefined,
> {
request: NextRequest;
user: RequireAuth extends false ? undefined : User;
body: Schema extends z.ZodType ? z.infer<Schema> : undefined;
params: Record<string, string>;
}
/**
* Enhanced route handler function.
*
* This function takes a request and parameters object as arguments and returns a route handler function.
* The route handler function can be used to handle HTTP requests and apply additional enhancements
* based on the provided parameters.
*
* Usage:
* export const POST = enhanceRouteHandler(
* ({ request, body, user }) => {
* return new Response(`Hello, ${body.name}!`);
* },
* {
* schema: z.object({
* name: z.string(),
* }),
* },
* );
*
*/
export const enhanceRouteHandler = <
Body,
Params extends Config<z.ZodType<Body, z.ZodTypeDef>>,
>(
// Route handler function
handler:
| ((
params: HandlerParams<Params['schema'], Params['auth']>,
) => NextResponse | Response)
| ((
params: HandlerParams<Params['schema'], Params['auth']>,
) => Promise<NextResponse | Response>),
// Parameters object
params?: Params,
) => {
/**
* Route handler function.
*
* This function takes a request object as an argument and returns a response object.
*/
return async function routeHandler(
request: NextRequest,
routeParams: {
params: Promise<Record<string, string>>;
},
) {
type UserParam = Params['auth'] extends false ? undefined : User;
let user: UserParam = undefined as UserParam;
// Check if the captcha token should be verified
const shouldVerifyCaptcha = params?.captcha ?? false;
// Verify the captcha token if required and setup
if (shouldVerifyCaptcha) {
const token = captchaTokenGetter(request);
// If the captcha token is not provided, return a 400 response.
if (token) {
await verifyCaptchaToken(token);
} else {
return new Response('Captcha token is required', { status: 400 });
}
}
const client = getSupabaseServerClient();
const shouldVerifyAuth = params?.auth ?? true;
// 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;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let body: any;
if (params?.schema) {
// clone the request to read the body
// so that we can pass it to the handler safely
const json = await request.clone().json();
body = zodParseFactory(params.schema)(json);
}
return handler({
request,
body,
user,
params: await routeParams.params,
});
};
};
/**
* Get the captcha token from the request headers.
* @param request
*/
function captchaTokenGetter(request: NextRequest) {
return request.headers.get('x-captcha-token');
}

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
export const zodParseFactory =
<T extends z.ZodTypeAny>(schema: T) =>
(data: unknown): z.infer<T> => {
try {
return schema.parse(data) as unknown;
} catch (err) {
console.error(err);
// handle error
throw new Error(`Invalid data: ${err as string}`);
}
};