Unify workspace dropdowns; Update layouts (#458)
Unified Account and Workspace drop-downs; Layout updates, now header lives within the PageBody component; Sidebars now use floating variant
This commit is contained in:
committed by
GitHub
parent
ca585e09be
commit
4bc8448a1d
@@ -2,10 +2,12 @@
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Function | Import | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `enhanceAction` | `@kit/next/actions` | Server actions with auth + validation |
|
||||
| `enhanceRouteHandler` | `@kit/next/routes` | API routes with auth + validation |
|
||||
| Function | Import | Purpose |
|
||||
|-----------------------|-------------------------|------------------------------------|
|
||||
| `authActionClient` | `@kit/next/safe-action` | Authenticated server actions |
|
||||
| `publicActionClient` | `@kit/next/safe-action` | Public server actions (no auth) |
|
||||
| `captchaActionClient` | `@kit/next/safe-action` | Server actions with CAPTCHA + auth |
|
||||
| `enhanceRouteHandler` | `@kit/next/routes` | API routes with auth + validation |
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -14,29 +16,78 @@
|
||||
- Authorization via RLS, not application code
|
||||
- Use `'use server'` at top of file
|
||||
- Always validate with Zod schema
|
||||
- Use `useAction` hook from `next-safe-action/hooks` in client components
|
||||
|
||||
## Skills
|
||||
|
||||
For detailed implementation patterns:
|
||||
- `/server-action-builder` - Complete server action workflow
|
||||
|
||||
## Server Action Pattern
|
||||
## Server Action Pattern (next-safe-action)
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
|
||||
export const myAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
// data: validated, user: authenticated
|
||||
// Authenticated action with schema validation
|
||||
export const myAction = authActionClient
|
||||
.schema(MySchema)
|
||||
.action(async ({ parsedInput: data, ctx: { user } }) => {
|
||||
// data: validated input, user: authenticated user
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: MySchema,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Public action (no auth required)
|
||||
import { publicActionClient } from '@kit/next/safe-action';
|
||||
|
||||
export const publicAction = publicActionClient
|
||||
.schema(MySchema)
|
||||
.action(async ({ parsedInput: data }) => {
|
||||
return { success: true };
|
||||
});
|
||||
```
|
||||
|
||||
### Admin actions
|
||||
|
||||
Admin actions use a dedicated client in `@kit/admin`:
|
||||
|
||||
```typescript
|
||||
import { adminActionClient } from '../utils/admin-action-client';
|
||||
|
||||
export const adminAction = adminActionClient
|
||||
.schema(MySchema)
|
||||
.action(async ({ parsedInput: data, ctx: { user } }) => {
|
||||
// Only accessible to super admins
|
||||
return { success: true };
|
||||
});
|
||||
```
|
||||
|
||||
## Client Component Pattern (useAction)
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { myAction } from '../server/server-actions';
|
||||
|
||||
function MyComponent() {
|
||||
const { execute, isPending, hasErrored, result } = useAction(myAction, {
|
||||
onSuccess: ({ data }) => {
|
||||
// Handle success
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
// Handle error
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); execute(formData); }}>
|
||||
{/* form fields */}
|
||||
<button disabled={isPending}>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Route Handler Pattern
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
"./actions": "./src/actions/index.ts",
|
||||
"./safe-action": "./src/actions/safe-action-client.ts",
|
||||
"./routes": "./src/routes/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"next-safe-action": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/auth": "workspace:*",
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
|
||||
@@ -20,19 +20,22 @@ export function enhanceAction<
|
||||
auth?: boolean;
|
||||
captcha?: boolean;
|
||||
schema?: z.ZodType<
|
||||
Config['captcha'] extends true ? Args & { captchaToken: string } : Args,
|
||||
z.ZodTypeDef
|
||||
Config['captcha'] extends true ? Args & { captchaToken: string } : Args
|
||||
>;
|
||||
},
|
||||
>(
|
||||
fn: (
|
||||
params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
|
||||
params: Config['schema'] extends ZodType
|
||||
? z.output<Config['schema']>
|
||||
: Args,
|
||||
user: Config['auth'] extends false ? undefined : JWTUserData,
|
||||
) => Response | Promise<Response>,
|
||||
config: Config,
|
||||
) {
|
||||
return async (
|
||||
params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
|
||||
params: Config['schema'] extends ZodType
|
||||
? z.output<Config['schema']>
|
||||
: Args,
|
||||
) => {
|
||||
type UserParam = Config['auth'] extends false ? undefined : JWTUserData;
|
||||
|
||||
@@ -80,6 +83,11 @@ export function enhanceAction<
|
||||
user = auth.data as UserParam;
|
||||
}
|
||||
|
||||
return fn(data, user);
|
||||
return fn(
|
||||
data as Config['schema'] extends ZodType
|
||||
? z.output<Config['schema']>
|
||||
: Args,
|
||||
user,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
55
packages/next/src/actions/safe-action-client.ts
Normal file
55
packages/next/src/actions/safe-action-client.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'server-only';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { createSafeActionClient } from 'next-safe-action';
|
||||
|
||||
import { verifyCaptchaToken } from '@kit/auth/captcha/server';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
const baseClient = createSafeActionClient({
|
||||
handleServerError: (error) => error.message,
|
||||
});
|
||||
|
||||
/**
|
||||
* @name publicActionClient
|
||||
* @description Safe action client for public actions that don't require authentication.
|
||||
*/
|
||||
export const publicActionClient = baseClient;
|
||||
|
||||
/**
|
||||
* @name authActionClient
|
||||
* @description Safe action client for authenticated actions. Adds user context.
|
||||
*/
|
||||
export const authActionClient = baseClient.use(async ({ next }) => {
|
||||
const auth = await requireUser(getSupabaseServerClient());
|
||||
|
||||
if (!auth.data) {
|
||||
redirect(auth.redirectTo);
|
||||
}
|
||||
|
||||
return next({ ctx: { user: auth.data } });
|
||||
});
|
||||
|
||||
/**
|
||||
* @name captchaActionClient
|
||||
* @description Safe action client for actions that require CAPTCHA and authentication.
|
||||
*/
|
||||
export const captchaActionClient = baseClient.use(
|
||||
async ({ next, clientInput }) => {
|
||||
const input = clientInput as Record<string, unknown>;
|
||||
const token =
|
||||
typeof input?.captchaToken === 'string' ? input.captchaToken : '';
|
||||
|
||||
await verifyCaptchaToken(token);
|
||||
|
||||
const auth = await requireUser(getSupabaseServerClient());
|
||||
|
||||
if (!auth.data) {
|
||||
redirect(auth.redirectTo);
|
||||
}
|
||||
|
||||
return next({ ctx: { user: auth.data } });
|
||||
},
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import 'server-only';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { verifyCaptchaToken } from '@kit/auth/captcha/server';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
@@ -22,7 +22,7 @@ interface HandlerParams<
|
||||
> {
|
||||
request: NextRequest;
|
||||
user: RequireAuth extends false ? undefined : JWTUserData;
|
||||
body: Schema extends z.ZodType ? z.infer<Schema> : undefined;
|
||||
body: Schema extends z.ZodType ? z.output<Schema> : undefined;
|
||||
params: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ interface HandlerParams<
|
||||
*/
|
||||
export const enhanceRouteHandler = <
|
||||
Body,
|
||||
Params extends Config<z.ZodType<Body, z.ZodTypeDef>>,
|
||||
Params extends Config<z.ZodType<Body>>,
|
||||
>(
|
||||
// Route handler function
|
||||
handler:
|
||||
|
||||
Reference in New Issue
Block a user