next-supa-utils 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -65
- package/dist/server/index.cjs +50 -16
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +58 -17
- package/dist/server/index.d.ts +58 -17
- package/dist/server/index.js +50 -16
- package/dist/server/index.js.map +1 -1
- package/dist/shared/index.cjs.map +1 -1
- package/dist/shared/index.d.cts +49 -12
- package/dist/shared/index.d.ts +49 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,47 +16,24 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
## Why
|
|
19
|
+
## 🚀 Why Use This Over Raw `@supabase/ssr`?
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
If you use `@supabase/ssr` directly, you have to write boilerplate for *every* environment. **next-supa-utils** eliminates this entirely:
|
|
22
22
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
|
|
23
|
+
1. **One-Line Middleware**: No more manually copying the 40-line chunking/cookie-setting logic from the Supabase docs. Just pass your routes to `withSupaAuth()`.
|
|
24
|
+
2. **Type-Safe Server Actions**: Stop writing `try/catch` and `cookies().getAll()` in every server action. `createAction()` handles it automatically and forces you to check for errors.
|
|
25
|
+
3. **Instant Client Hooks**: `useSupaUser()` and `useSupaSession()` wrap `createBrowserClient`, fetch the initial state, *and* subscribe to real-time `onAuthStateChange` events out of the box.
|
|
26
|
+
4. **App Router Ready**: Strictly separated entry points (`/client` and `/server`) guarantee you won't accidentally import server code into client components.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## Features
|
|
30
|
-
|
|
31
|
-
- 🔐 **`withSupaAuth`** — Drop-in middleware for route protection with redirect support
|
|
32
|
-
- ⚡ **`createAction`** — Higher-order function that wraps server actions with automatic Supabase client creation and error handling
|
|
33
|
-
- 👤 **`useSupaUser`** — React hook for real-time user state with auth change subscriptions
|
|
34
|
-
- 🔑 **`useSupaSession`** — React hook for real-time session state
|
|
35
|
-
- 🧩 **Separate entry points** — `next-supa-utils/client` and `next-supa-utils/server` to respect the `"use client"` boundary
|
|
36
|
-
- 📦 **Dual format** — Ships ESM and CJS with full TypeScript declarations
|
|
37
|
-
|
|
38
|
-
## Installation
|
|
28
|
+
## 📦 Installation
|
|
39
29
|
|
|
40
30
|
```bash
|
|
41
31
|
npm install next-supa-utils
|
|
42
32
|
```
|
|
43
33
|
|
|
44
|
-
|
|
34
|
+
*Requires `react >=18`, `next >=14`, `@supabase/supabase-js ^2`, and `@supabase/ssr >=0.5`.*
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm install react next @supabase/supabase-js @supabase/ssr
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
| Package | Version |
|
|
53
|
-
|---|---|
|
|
54
|
-
| `react` | `>=18` |
|
|
55
|
-
| `next` | `>=14` |
|
|
56
|
-
| `@supabase/supabase-js` | `^2.0.0` |
|
|
57
|
-
| `@supabase/ssr` | `>=0.5.0` |
|
|
58
|
-
|
|
59
|
-
## Environment Variables
|
|
36
|
+
### Environment Variables
|
|
60
37
|
|
|
61
38
|
Add these to your `.env.local`:
|
|
62
39
|
|
|
@@ -65,73 +42,61 @@ NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
|
|
65
42
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
|
66
43
|
```
|
|
67
44
|
|
|
68
|
-
Both variables are required. All helpers in this library read from these environment variables automatically.
|
|
69
|
-
|
|
70
45
|
---
|
|
71
46
|
|
|
72
|
-
## Quick Start
|
|
47
|
+
## ⚡ Quick Start
|
|
73
48
|
|
|
74
|
-
### 1.
|
|
49
|
+
### 1. Middleware (Route Protection in 1 Line)
|
|
50
|
+
|
|
51
|
+
Protect your routes and auto-refresh sessions without copying boilerplate.
|
|
75
52
|
|
|
76
53
|
```ts
|
|
77
54
|
// middleware.ts
|
|
78
55
|
import { withSupaAuth } from "next-supa-utils/server";
|
|
79
56
|
|
|
80
57
|
export default withSupaAuth({
|
|
81
|
-
protectedRoutes: ["/dashboard", "/admin"
|
|
58
|
+
protectedRoutes: ["/dashboard", "/admin"],
|
|
82
59
|
redirectTo: "/login",
|
|
83
|
-
publicRoutes: ["/admin/login"],
|
|
84
60
|
});
|
|
85
61
|
|
|
86
|
-
export const config = {
|
|
87
|
-
matcher: [
|
|
88
|
-
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
|
89
|
-
],
|
|
90
|
-
};
|
|
62
|
+
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };
|
|
91
63
|
```
|
|
92
64
|
|
|
93
|
-
### 2.
|
|
65
|
+
### 2. Server Actions (No More Try/Catch)
|
|
66
|
+
|
|
67
|
+
Automatically initializes the server client with cookies and returns a type-safe `{ data, error }` object.
|
|
94
68
|
|
|
95
69
|
```ts
|
|
96
|
-
// app/actions
|
|
70
|
+
// app/actions.ts
|
|
97
71
|
"use server";
|
|
98
72
|
import { createAction } from "next-supa-utils/server";
|
|
99
73
|
|
|
100
74
|
export const getProfile = createAction(async (supabase, userId: string) => {
|
|
101
|
-
const { data, error } = await supabase
|
|
102
|
-
|
|
103
|
-
.select("*")
|
|
104
|
-
.eq("id", userId)
|
|
105
|
-
.single();
|
|
106
|
-
|
|
107
|
-
if (error) throw error;
|
|
75
|
+
const { data, error } = await supabase.from("profiles").select().eq("id", userId).single();
|
|
76
|
+
if (error) throw error; // Handled automatically!
|
|
108
77
|
return data;
|
|
109
78
|
});
|
|
110
79
|
```
|
|
111
80
|
|
|
112
81
|
```tsx
|
|
113
|
-
// Usage
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
if (result.error) {
|
|
117
|
-
console.error(result.error.message);
|
|
118
|
-
} else {
|
|
119
|
-
console.log(result.data);
|
|
120
|
-
}
|
|
82
|
+
// Usage
|
|
83
|
+
const { data, error } = await getProfile("123");
|
|
84
|
+
if (error) console.error(error.message);
|
|
121
85
|
```
|
|
122
86
|
|
|
123
|
-
### 3.
|
|
87
|
+
### 3. Client Components (Real-Time User State)
|
|
88
|
+
|
|
89
|
+
Get the user and listen to auth changes immediately.
|
|
124
90
|
|
|
125
91
|
```tsx
|
|
126
92
|
"use client";
|
|
127
93
|
import { useSupaUser } from "next-supa-utils/client";
|
|
128
94
|
|
|
129
95
|
export default function Avatar() {
|
|
130
|
-
const { user, loading
|
|
96
|
+
const { user, loading } = useSupaUser();
|
|
131
97
|
|
|
132
|
-
if (loading) return <p>Loading
|
|
133
|
-
if (
|
|
134
|
-
if (!user) return <p>Not signed in</p>;
|
|
98
|
+
if (loading) return <p>Loading...</p>;
|
|
99
|
+
if (!user) return <p>Please sign in</p>;
|
|
135
100
|
|
|
136
101
|
return <p>Hello, {user.email}!</p>;
|
|
137
102
|
}
|
|
@@ -307,6 +272,31 @@ src/
|
|
|
307
272
|
| `next-supa-utils/server` | Server Components, Middleware, Server Actions | `withSupaAuth`, `createAction` |
|
|
308
273
|
| `next-supa-utils` | Anywhere | `handleSupaError`, all types |
|
|
309
274
|
|
|
275
|
+
## Changelog
|
|
276
|
+
|
|
277
|
+
### v0.1.3
|
|
278
|
+
|
|
279
|
+
- **🔐 RBAC Support** — `withSupaAuth` now supports **role-based access control**.
|
|
280
|
+
- New `RouteConfig` type: define `allowedRoles` per route (e.g. `["admin", "editor"]`).
|
|
281
|
+
- New `MiddlewareOptions` replaces the old `SupaAuthConfig`.
|
|
282
|
+
- New `roleExtractor` option: read roles from `user_metadata`, `app_metadata`, or a custom function.
|
|
283
|
+
- Wildcard path matching supported (e.g. `"/admin/:path*"`).
|
|
284
|
+
- Unauthorized users are redirected with `?error=forbidden&next=<path>`.
|
|
285
|
+
- **⚠️ Breaking:** `SupaAuthConfig` type removed. Use `MiddlewareOptions` with `routes: RouteConfig[]` instead.
|
|
286
|
+
|
|
287
|
+
### v0.1.0
|
|
288
|
+
|
|
289
|
+
- 🎉 **Initial release**
|
|
290
|
+
- `withSupaAuth` — Middleware helper for route protection with session refresh.
|
|
291
|
+
- `createAction` — Type-safe server action wrapper with automatic `{ data, error }` responses.
|
|
292
|
+
- `useSupaUser` — React hook for real-time user state.
|
|
293
|
+
- `useSupaSession` — React hook for real-time session state.
|
|
294
|
+
- `handleSupaError` — Universal error normalizer.
|
|
295
|
+
- Multi-entry package: `next-supa-utils/client`, `next-supa-utils/server`.
|
|
296
|
+
- Dual format output (ESM + CJS) with TypeScript declarations.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
310
300
|
## Contributing
|
|
311
301
|
|
|
312
302
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/server/index.cjs
CHANGED
|
@@ -28,13 +28,41 @@ module.exports = __toCommonJS(server_exports);
|
|
|
28
28
|
// src/server/middleware/withSupaAuth.ts
|
|
29
29
|
var import_ssr = require("@supabase/ssr");
|
|
30
30
|
var import_server = require("next/server");
|
|
31
|
-
function
|
|
31
|
+
function matchPath(pattern, pathname) {
|
|
32
|
+
if (pattern.includes(":path*")) {
|
|
33
|
+
const prefix = pattern.replace(/:path\*$/, "").replace(/\/$/, "");
|
|
34
|
+
return pathname === prefix || pathname.startsWith(prefix + "/");
|
|
35
|
+
}
|
|
36
|
+
return pathname === pattern || pathname.startsWith(pattern + "/");
|
|
37
|
+
}
|
|
38
|
+
function findMatchingRoute(routes, pathname) {
|
|
39
|
+
return routes.find((route) => matchPath(route.path, pathname));
|
|
40
|
+
}
|
|
41
|
+
function extractRole(user, extractor) {
|
|
42
|
+
if (typeof extractor === "function") {
|
|
43
|
+
return extractor(user);
|
|
44
|
+
}
|
|
45
|
+
const source = extractor === "app_metadata" ? user.app_metadata : user.user_metadata;
|
|
46
|
+
const raw = source?.role;
|
|
47
|
+
if (typeof raw === "string") return raw;
|
|
48
|
+
if (Array.isArray(raw) && raw.every((r) => typeof r === "string")) {
|
|
49
|
+
return raw;
|
|
50
|
+
}
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
function hasRequiredRole(userRole, allowedRoles) {
|
|
54
|
+
if (!allowedRoles || allowedRoles.length === 0) return true;
|
|
55
|
+
if (!userRole) return false;
|
|
56
|
+
const roles = Array.isArray(userRole) ? userRole : [userRole];
|
|
57
|
+
return roles.some((r) => allowedRoles.includes(r));
|
|
58
|
+
}
|
|
59
|
+
function withSupaAuth(options) {
|
|
32
60
|
const {
|
|
33
|
-
|
|
61
|
+
routes,
|
|
34
62
|
redirectTo = "/login",
|
|
35
|
-
|
|
63
|
+
roleExtractor = "user_metadata",
|
|
36
64
|
onAuthSuccess
|
|
37
|
-
} =
|
|
65
|
+
} = options;
|
|
38
66
|
return async function middleware(request) {
|
|
39
67
|
let response = import_server.NextResponse.next({
|
|
40
68
|
request: { headers: request.headers }
|
|
@@ -57,8 +85,8 @@ function withSupaAuth(config) {
|
|
|
57
85
|
request.cookies.set(name, value);
|
|
58
86
|
});
|
|
59
87
|
response = import_server.NextResponse.next({ request });
|
|
60
|
-
cookiesToSet.forEach(({ name, value, options }) => {
|
|
61
|
-
response.cookies.set(name, value,
|
|
88
|
+
cookiesToSet.forEach(({ name, value, options: options2 }) => {
|
|
89
|
+
response.cookies.set(name, value, options2);
|
|
62
90
|
});
|
|
63
91
|
}
|
|
64
92
|
}
|
|
@@ -67,22 +95,28 @@ function withSupaAuth(config) {
|
|
|
67
95
|
data: { user }
|
|
68
96
|
} = await supabase.auth.getUser();
|
|
69
97
|
const { pathname } = request.nextUrl;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
if (isPublicRoute) {
|
|
98
|
+
const matchedRoute = findMatchingRoute(routes, pathname);
|
|
99
|
+
if (!matchedRoute) {
|
|
74
100
|
return response;
|
|
75
101
|
}
|
|
76
|
-
|
|
77
|
-
(route) => pathname.startsWith(route)
|
|
78
|
-
);
|
|
79
|
-
if (isProtectedRoute && !user) {
|
|
102
|
+
if (!user) {
|
|
80
103
|
const loginUrl = new URL(redirectTo, request.url);
|
|
81
104
|
loginUrl.searchParams.set("next", pathname);
|
|
82
105
|
return import_server.NextResponse.redirect(loginUrl);
|
|
83
106
|
}
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
const userRole = extractRole(user, roleExtractor);
|
|
108
|
+
if (!hasRequiredRole(userRole, matchedRoute.allowedRoles)) {
|
|
109
|
+
const forbiddenUrl = new URL(redirectTo, request.url);
|
|
110
|
+
forbiddenUrl.searchParams.set("error", "forbidden");
|
|
111
|
+
forbiddenUrl.searchParams.set("next", pathname);
|
|
112
|
+
return import_server.NextResponse.redirect(forbiddenUrl);
|
|
113
|
+
}
|
|
114
|
+
if (onAuthSuccess) {
|
|
115
|
+
await onAuthSuccess({
|
|
116
|
+
id: user.id,
|
|
117
|
+
email: user.email ?? void 0,
|
|
118
|
+
role: userRole
|
|
119
|
+
});
|
|
86
120
|
}
|
|
87
121
|
return response;
|
|
88
122
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/index.ts","../../src/server/middleware/withSupaAuth.ts","../../src/server/actions/actionWrapper.ts","../../src/shared/utils/error-handler.ts"],"sourcesContent":["// ── Server entry point ──────────────────────────────────────────────\n// This module should ONLY be imported in server-side contexts:\n// middleware.ts, Server Components, Server Actions, Route Handlers.\n\nexport { withSupaAuth } from \"./middleware/withSupaAuth\";\nexport { createAction } from \"./actions/actionWrapper\";\n\n// Re-export types consumers commonly need alongside server helpers.\nexport type { SupaAuthConfig, ActionResponse, SupaError } from \"../types\";\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { NextResponse, type NextRequest } from \"next/server\";\n\nimport type { SupaAuthConfig } from \"../../types\";\n\n/**\n * Create a Next.js middleware handler that protects routes based on\n * Supabase authentication state.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { withSupaAuth } from \"next-supa-utils/server\";\n *\n * export default withSupaAuth({\n * protectedRoutes: [\"/dashboard\", \"/admin\"],\n * redirectTo: \"/login\",\n * publicRoutes: [\"/admin/login\"],\n * });\n *\n * export const config = {\n * matcher: [\"/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)\"],\n * };\n * ```\n */\nexport function withSupaAuth(config: SupaAuthConfig) {\n const {\n protectedRoutes,\n redirectTo = \"/login\",\n publicRoutes = [],\n onAuthSuccess,\n } = config;\n\n return async function middleware(request: NextRequest): Promise<NextResponse> {\n // ── 1. Create a mutable response so Supabase can set cookies ──\n let response = NextResponse.next({\n request: { headers: request.headers },\n });\n\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n console.error(\n \"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n return response;\n }\n\n // ── 2. Initialize server client with middleware cookie helpers ─\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return request.cookies.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n // Forward cookies to the request so downstream server\n // components can read the updated session.\n cookiesToSet.forEach(({ name, value }) => {\n request.cookies.set(name, value);\n });\n\n // Re-create the response so it carries the updated request.\n response = NextResponse.next({ request });\n\n // Set cookies on the outgoing response so the browser\n // stores the refreshed tokens.\n cookiesToSet.forEach(({ name, value, options }) => {\n response.cookies.set(name, value, options);\n });\n },\n },\n });\n\n // ── 3. Refresh session (required to keep tokens alive) ────────\n const {\n data: { user },\n } = await supabase.auth.getUser();\n\n const { pathname } = request.nextUrl;\n\n // ── 4. Check if current path matches a public override ────────\n const isPublicRoute = publicRoutes.some((route) =>\n pathname.startsWith(route),\n );\n\n if (isPublicRoute) {\n return response;\n }\n\n // ── 5. Check if current path requires authentication ──────────\n const isProtectedRoute = protectedRoutes.some((route) =>\n pathname.startsWith(route),\n );\n\n if (isProtectedRoute && !user) {\n const loginUrl = new URL(redirectTo, request.url);\n // Preserve the originally-requested URL so the app can redirect\n // back after login.\n loginUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(loginUrl);\n }\n\n // ── 6. Optional success callback ──────────────────────────────\n if (user && onAuthSuccess) {\n await onAuthSuccess({ id: user.id, email: user.email ?? undefined });\n }\n\n return response;\n };\n}\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nimport type { ActionResponse, SupaError } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * Create a type-safe Server Action that automatically:\n * 1. Initialises a Supabase server client (with cookies)\n * 2. Wraps execution in try/catch\n * 3. Returns a standardised `{ data, error }` response\n *\n * @example\n * ```ts\n * // app/actions/profile.ts\n * \"use server\";\n * import { createAction } from \"next-supa-utils/server\";\n *\n * export const getProfile = createAction(async (supabase, userId: string) => {\n * const { data, error } = await supabase\n * .from(\"profiles\")\n * .select(\"*\")\n * .eq(\"id\", userId)\n * .single();\n *\n * if (error) throw error;\n * return data;\n * });\n *\n * // Usage in a Server Component or Client Component:\n * const result = await getProfile(\"user-uuid\");\n * if (result.error) { ... }\n * ```\n */\nexport function createAction<TArgs extends unknown[], TResult>(\n fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>,\n): (...args: TArgs) => Promise<ActionResponse<TResult>> {\n return async (...args: TArgs): Promise<ActionResponse<TResult>> => {\n try {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n return {\n data: null,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n };\n }\n\n const cookieStore = await cookies();\n\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n try {\n cookiesToSet.forEach(({ name, value, options }) => {\n cookieStore.set(name, value, options);\n });\n } catch {\n // `cookies().set()` throws when called from a Server Component.\n // In that context we only need read access — the middleware\n // handles token refresh.\n }\n },\n },\n });\n\n const data = await fn(supabase, ...args);\n\n return { data, error: null };\n } catch (caught: unknown) {\n const error: SupaError = handleSupaError(caught);\n return { data: null, error };\n }\n };\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n"],"mappings":"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","names":["import_ssr"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts","../../src/server/middleware/withSupaAuth.ts","../../src/server/actions/actionWrapper.ts","../../src/shared/utils/error-handler.ts"],"sourcesContent":["// ── Server entry point ──────────────────────────────────────────────\n// This module should ONLY be imported in server-side contexts:\n// middleware.ts, Server Components, Server Actions, Route Handlers.\n\nexport { withSupaAuth } from \"./middleware/withSupaAuth\";\nexport { createAction } from \"./actions/actionWrapper\";\n\n// Re-export types consumers commonly need alongside server helpers.\nexport type { MiddlewareOptions, RouteConfig, ActionResponse, SupaError } from \"../types\";\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { NextResponse, type NextRequest } from \"next/server\";\n\nimport type { MiddlewareOptions, RouteConfig } from \"../../types\";\n\n// ── Path matching utility ───────────────────────────────────────────\n\n/**\n * Converts a route pattern into a RegExp for matching.\n *\n * Supports:\n * - Exact: `\"/settings\"` → matches only `/settings`\n * - Prefix: `\"/dashboard\"` → matches `/dashboard`, `/dashboard/stats`\n * - Wildcard: `\"/admin/:path*\"` → matches `/admin`, `/admin/users`, `/admin/a/b/c`\n */\nfunction matchPath(pattern: string, pathname: string): boolean {\n // Wildcard pattern: \"/admin/:path*\" → match \"/admin\" and everything below\n if (pattern.includes(\":path*\")) {\n const prefix = pattern.replace(/:path\\*$/, \"\").replace(/\\/$/, \"\");\n return pathname === prefix || pathname.startsWith(prefix + \"/\");\n }\n\n // Prefix matching: \"/dashboard\" matches \"/dashboard/anything\"\n return pathname === pattern || pathname.startsWith(pattern + \"/\");\n}\n\n/**\n * Finds the first `RouteConfig` whose pattern matches `pathname`, or `undefined`.\n */\nfunction findMatchingRoute(\n routes: RouteConfig[],\n pathname: string,\n): RouteConfig | undefined {\n return routes.find((route) => matchPath(route.path, pathname));\n}\n\n// ── Role extraction utility ─────────────────────────────────────────\n\ntype UserMeta = {\n user_metadata: Record<string, unknown>;\n app_metadata: Record<string, unknown>;\n};\n\nfunction extractRole(\n user: UserMeta,\n extractor: MiddlewareOptions[\"roleExtractor\"],\n): string | string[] | undefined {\n if (typeof extractor === \"function\") {\n return extractor(user);\n }\n\n const source =\n extractor === \"app_metadata\" ? user.app_metadata : user.user_metadata;\n\n const raw = source?.role;\n\n if (typeof raw === \"string\") return raw;\n if (Array.isArray(raw) && raw.every((r) => typeof r === \"string\")) {\n return raw as string[];\n }\n\n return undefined;\n}\n\n/**\n * Check whether the user's role(s) satisfy the route's `allowedRoles`.\n */\nfunction hasRequiredRole(\n userRole: string | string[] | undefined,\n allowedRoles: string[] | undefined,\n): boolean {\n // No role restriction → any authenticated user is allowed.\n if (!allowedRoles || allowedRoles.length === 0) return true;\n\n if (!userRole) return false;\n\n const roles = Array.isArray(userRole) ? userRole : [userRole];\n return roles.some((r) => allowedRoles.includes(r));\n}\n\n// ── Main middleware factory ─────────────────────────────────────────\n\n/**\n * Create a Next.js middleware handler that protects routes with\n * authentication **and** optional role-based access control (RBAC).\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { withSupaAuth } from \"next-supa-utils/server\";\n *\n * export default withSupaAuth({\n * routes: [\n * { path: \"/dashboard\" }, // any authed user\n * { path: \"/admin/:path*\", allowedRoles: [\"admin\"] }, // admin only\n * { path: \"/editor/:path*\", allowedRoles: [\"admin\", \"editor\"] },\n * ],\n * redirectTo: \"/login\",\n * roleExtractor: \"user_metadata\", // or \"app_metadata\" or a custom fn\n * });\n *\n * export const config = {\n * matcher: [\"/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)\"],\n * };\n * ```\n */\nexport function withSupaAuth(options: MiddlewareOptions) {\n const {\n routes,\n redirectTo = \"/login\",\n roleExtractor = \"user_metadata\",\n onAuthSuccess,\n } = options;\n\n return async function middleware(request: NextRequest): Promise<NextResponse> {\n // ── 1. Create a mutable response so Supabase can set cookies ──\n let response = NextResponse.next({\n request: { headers: request.headers },\n });\n\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n console.error(\n \"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n return response;\n }\n\n // ── 2. Initialize server client with middleware cookie helpers ─\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return request.cookies.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n cookiesToSet.forEach(({ name, value }) => {\n request.cookies.set(name, value);\n });\n\n response = NextResponse.next({ request });\n\n cookiesToSet.forEach(({ name, value, options }) => {\n response.cookies.set(name, value, options);\n });\n },\n },\n });\n\n // ── 3. Refresh session (required to keep tokens alive) ────────\n const {\n data: { user },\n } = await supabase.auth.getUser();\n\n const { pathname } = request.nextUrl;\n\n // ── 4. Find the matching route config ─────────────────────────\n const matchedRoute = findMatchingRoute(routes, pathname);\n\n // No matching route → not a protected path, pass through.\n if (!matchedRoute) {\n return response;\n }\n\n // ── 5. Check authentication ───────────────────────────────────\n if (!user) {\n const loginUrl = new URL(redirectTo, request.url);\n loginUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(loginUrl);\n }\n\n // ── 6. Check role-based access ────────────────────────────────\n const userRole = extractRole(user, roleExtractor);\n\n if (!hasRequiredRole(userRole, matchedRoute.allowedRoles)) {\n // User is logged in but lacks the required role.\n const forbiddenUrl = new URL(redirectTo, request.url);\n forbiddenUrl.searchParams.set(\"error\", \"forbidden\");\n forbiddenUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(forbiddenUrl);\n }\n\n // ── 7. Success callback ───────────────────────────────────────\n if (onAuthSuccess) {\n await onAuthSuccess({\n id: user.id,\n email: user.email ?? undefined,\n role: userRole,\n });\n }\n\n return response;\n };\n}\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nimport type { ActionResponse, SupaError } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * Create a type-safe Server Action that automatically:\n * 1. Initialises a Supabase server client (with cookies)\n * 2. Wraps execution in try/catch\n * 3. Returns a standardised `{ data, error }` response\n *\n * @example\n * ```ts\n * // app/actions/profile.ts\n * \"use server\";\n * import { createAction } from \"next-supa-utils/server\";\n *\n * export const getProfile = createAction(async (supabase, userId: string) => {\n * const { data, error } = await supabase\n * .from(\"profiles\")\n * .select(\"*\")\n * .eq(\"id\", userId)\n * .single();\n *\n * if (error) throw error;\n * return data;\n * });\n *\n * // Usage in a Server Component or Client Component:\n * const result = await getProfile(\"user-uuid\");\n * if (result.error) { ... }\n * ```\n */\nexport function createAction<TArgs extends unknown[], TResult>(\n fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>,\n): (...args: TArgs) => Promise<ActionResponse<TResult>> {\n return async (...args: TArgs): Promise<ActionResponse<TResult>> => {\n try {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n return {\n data: null,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n };\n }\n\n const cookieStore = await cookies();\n\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n try {\n cookiesToSet.forEach(({ name, value, options }) => {\n cookieStore.set(name, value, options);\n });\n } catch {\n // `cookies().set()` throws when called from a Server Component.\n // In that context we only need read access — the middleware\n // handles token refresh.\n }\n },\n },\n });\n\n const data = await fn(supabase, ...args);\n\n return { data, error: null };\n } catch (caught: unknown) {\n const error: SupaError = handleSupaError(caught);\n return { data: null, error };\n }\n };\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n"],"mappings":"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","names":["options","import_ssr"]}
|
package/dist/server/index.d.cts
CHANGED
|
@@ -15,38 +15,75 @@ type ActionResponse<T> = {
|
|
|
15
15
|
data: null;
|
|
16
16
|
error: SupaError;
|
|
17
17
|
};
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Defines a protected route with optional role-based access control.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* { path: "/admin/:path*", allowedRoles: ["admin", "super_admin"] }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
interface RouteConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Route pattern to match.
|
|
29
|
+
*
|
|
30
|
+
* Supports:
|
|
31
|
+
* - Exact paths: `"/settings"`
|
|
32
|
+
* - Prefix matching: `"/dashboard"` matches `/dashboard/anything`
|
|
33
|
+
* - Wildcards: `"/admin/:path*"` matches `/admin/users`, `/admin/settings/edit`, etc.
|
|
34
|
+
*/
|
|
35
|
+
path: string;
|
|
19
36
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
37
|
+
* Roles that are allowed to access this route.
|
|
38
|
+
*
|
|
39
|
+
* - If **omitted or empty**, any *authenticated* user can access the route.
|
|
40
|
+
* - If **provided**, only users whose role is in this list are allowed.
|
|
22
41
|
*
|
|
23
|
-
* @example ["
|
|
42
|
+
* @example ["admin", "editor"]
|
|
43
|
+
*/
|
|
44
|
+
allowedRoles?: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Configuration for the `withSupaAuth` middleware.
|
|
48
|
+
*/
|
|
49
|
+
interface MiddlewareOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Protected route definitions. Each entry can optionally restrict access
|
|
52
|
+
* to specific roles via `allowedRoles`.
|
|
24
53
|
*/
|
|
25
|
-
|
|
54
|
+
routes: RouteConfig[];
|
|
26
55
|
/**
|
|
27
|
-
* Where to redirect unauthenticated users.
|
|
56
|
+
* Where to redirect unauthenticated or unauthorized users.
|
|
28
57
|
* @default "/login"
|
|
29
58
|
*/
|
|
30
59
|
redirectTo?: string;
|
|
31
60
|
/**
|
|
32
|
-
*
|
|
61
|
+
* Strategy for extracting the user's role from the Supabase user object.
|
|
62
|
+
*
|
|
63
|
+
* - `"user_metadata"` — reads `user.user_metadata.role`
|
|
64
|
+
* - `"app_metadata"` — reads `user.app_metadata.role`
|
|
65
|
+
* - A custom function for advanced scenarios (e.g. multiple roles, JWT claims).
|
|
33
66
|
*
|
|
34
|
-
* @
|
|
67
|
+
* @default "user_metadata"
|
|
35
68
|
*/
|
|
36
|
-
|
|
69
|
+
roleExtractor?: "user_metadata" | "app_metadata" | ((user: {
|
|
70
|
+
user_metadata: Record<string, unknown>;
|
|
71
|
+
app_metadata: Record<string, unknown>;
|
|
72
|
+
}) => string | string[] | undefined);
|
|
37
73
|
/**
|
|
38
|
-
* Optional callback invoked
|
|
39
|
-
*
|
|
74
|
+
* Optional callback invoked when a user is authenticated and authorized.
|
|
75
|
+
* Useful for logging, analytics, or injecting custom response headers.
|
|
40
76
|
*/
|
|
41
77
|
onAuthSuccess?: (user: {
|
|
42
78
|
id: string;
|
|
43
79
|
email?: string;
|
|
80
|
+
role?: string | string[];
|
|
44
81
|
}) => void | Promise<void>;
|
|
45
82
|
}
|
|
46
83
|
|
|
47
84
|
/**
|
|
48
|
-
* Create a Next.js middleware handler that protects routes
|
|
49
|
-
*
|
|
85
|
+
* Create a Next.js middleware handler that protects routes with
|
|
86
|
+
* authentication **and** optional role-based access control (RBAC).
|
|
50
87
|
*
|
|
51
88
|
* @example
|
|
52
89
|
* ```ts
|
|
@@ -54,9 +91,13 @@ interface SupaAuthConfig {
|
|
|
54
91
|
* import { withSupaAuth } from "next-supa-utils/server";
|
|
55
92
|
*
|
|
56
93
|
* export default withSupaAuth({
|
|
57
|
-
*
|
|
94
|
+
* routes: [
|
|
95
|
+
* { path: "/dashboard" }, // any authed user
|
|
96
|
+
* { path: "/admin/:path*", allowedRoles: ["admin"] }, // admin only
|
|
97
|
+
* { path: "/editor/:path*", allowedRoles: ["admin", "editor"] },
|
|
98
|
+
* ],
|
|
58
99
|
* redirectTo: "/login",
|
|
59
|
-
*
|
|
100
|
+
* roleExtractor: "user_metadata", // or "app_metadata" or a custom fn
|
|
60
101
|
* });
|
|
61
102
|
*
|
|
62
103
|
* export const config = {
|
|
@@ -64,7 +105,7 @@ interface SupaAuthConfig {
|
|
|
64
105
|
* };
|
|
65
106
|
* ```
|
|
66
107
|
*/
|
|
67
|
-
declare function withSupaAuth(
|
|
108
|
+
declare function withSupaAuth(options: MiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
68
109
|
|
|
69
110
|
/**
|
|
70
111
|
* Create a type-safe Server Action that automatically:
|
|
@@ -96,4 +137,4 @@ declare function withSupaAuth(config: SupaAuthConfig): (request: NextRequest) =>
|
|
|
96
137
|
*/
|
|
97
138
|
declare function createAction<TArgs extends unknown[], TResult>(fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>): (...args: TArgs) => Promise<ActionResponse<TResult>>;
|
|
98
139
|
|
|
99
|
-
export { type ActionResponse, type
|
|
140
|
+
export { type ActionResponse, type MiddlewareOptions, type RouteConfig, type SupaError, createAction, withSupaAuth };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -15,38 +15,75 @@ type ActionResponse<T> = {
|
|
|
15
15
|
data: null;
|
|
16
16
|
error: SupaError;
|
|
17
17
|
};
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Defines a protected route with optional role-based access control.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* { path: "/admin/:path*", allowedRoles: ["admin", "super_admin"] }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
interface RouteConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Route pattern to match.
|
|
29
|
+
*
|
|
30
|
+
* Supports:
|
|
31
|
+
* - Exact paths: `"/settings"`
|
|
32
|
+
* - Prefix matching: `"/dashboard"` matches `/dashboard/anything`
|
|
33
|
+
* - Wildcards: `"/admin/:path*"` matches `/admin/users`, `/admin/settings/edit`, etc.
|
|
34
|
+
*/
|
|
35
|
+
path: string;
|
|
19
36
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
37
|
+
* Roles that are allowed to access this route.
|
|
38
|
+
*
|
|
39
|
+
* - If **omitted or empty**, any *authenticated* user can access the route.
|
|
40
|
+
* - If **provided**, only users whose role is in this list are allowed.
|
|
22
41
|
*
|
|
23
|
-
* @example ["
|
|
42
|
+
* @example ["admin", "editor"]
|
|
43
|
+
*/
|
|
44
|
+
allowedRoles?: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Configuration for the `withSupaAuth` middleware.
|
|
48
|
+
*/
|
|
49
|
+
interface MiddlewareOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Protected route definitions. Each entry can optionally restrict access
|
|
52
|
+
* to specific roles via `allowedRoles`.
|
|
24
53
|
*/
|
|
25
|
-
|
|
54
|
+
routes: RouteConfig[];
|
|
26
55
|
/**
|
|
27
|
-
* Where to redirect unauthenticated users.
|
|
56
|
+
* Where to redirect unauthenticated or unauthorized users.
|
|
28
57
|
* @default "/login"
|
|
29
58
|
*/
|
|
30
59
|
redirectTo?: string;
|
|
31
60
|
/**
|
|
32
|
-
*
|
|
61
|
+
* Strategy for extracting the user's role from the Supabase user object.
|
|
62
|
+
*
|
|
63
|
+
* - `"user_metadata"` — reads `user.user_metadata.role`
|
|
64
|
+
* - `"app_metadata"` — reads `user.app_metadata.role`
|
|
65
|
+
* - A custom function for advanced scenarios (e.g. multiple roles, JWT claims).
|
|
33
66
|
*
|
|
34
|
-
* @
|
|
67
|
+
* @default "user_metadata"
|
|
35
68
|
*/
|
|
36
|
-
|
|
69
|
+
roleExtractor?: "user_metadata" | "app_metadata" | ((user: {
|
|
70
|
+
user_metadata: Record<string, unknown>;
|
|
71
|
+
app_metadata: Record<string, unknown>;
|
|
72
|
+
}) => string | string[] | undefined);
|
|
37
73
|
/**
|
|
38
|
-
* Optional callback invoked
|
|
39
|
-
*
|
|
74
|
+
* Optional callback invoked when a user is authenticated and authorized.
|
|
75
|
+
* Useful for logging, analytics, or injecting custom response headers.
|
|
40
76
|
*/
|
|
41
77
|
onAuthSuccess?: (user: {
|
|
42
78
|
id: string;
|
|
43
79
|
email?: string;
|
|
80
|
+
role?: string | string[];
|
|
44
81
|
}) => void | Promise<void>;
|
|
45
82
|
}
|
|
46
83
|
|
|
47
84
|
/**
|
|
48
|
-
* Create a Next.js middleware handler that protects routes
|
|
49
|
-
*
|
|
85
|
+
* Create a Next.js middleware handler that protects routes with
|
|
86
|
+
* authentication **and** optional role-based access control (RBAC).
|
|
50
87
|
*
|
|
51
88
|
* @example
|
|
52
89
|
* ```ts
|
|
@@ -54,9 +91,13 @@ interface SupaAuthConfig {
|
|
|
54
91
|
* import { withSupaAuth } from "next-supa-utils/server";
|
|
55
92
|
*
|
|
56
93
|
* export default withSupaAuth({
|
|
57
|
-
*
|
|
94
|
+
* routes: [
|
|
95
|
+
* { path: "/dashboard" }, // any authed user
|
|
96
|
+
* { path: "/admin/:path*", allowedRoles: ["admin"] }, // admin only
|
|
97
|
+
* { path: "/editor/:path*", allowedRoles: ["admin", "editor"] },
|
|
98
|
+
* ],
|
|
58
99
|
* redirectTo: "/login",
|
|
59
|
-
*
|
|
100
|
+
* roleExtractor: "user_metadata", // or "app_metadata" or a custom fn
|
|
60
101
|
* });
|
|
61
102
|
*
|
|
62
103
|
* export const config = {
|
|
@@ -64,7 +105,7 @@ interface SupaAuthConfig {
|
|
|
64
105
|
* };
|
|
65
106
|
* ```
|
|
66
107
|
*/
|
|
67
|
-
declare function withSupaAuth(
|
|
108
|
+
declare function withSupaAuth(options: MiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
68
109
|
|
|
69
110
|
/**
|
|
70
111
|
* Create a type-safe Server Action that automatically:
|
|
@@ -96,4 +137,4 @@ declare function withSupaAuth(config: SupaAuthConfig): (request: NextRequest) =>
|
|
|
96
137
|
*/
|
|
97
138
|
declare function createAction<TArgs extends unknown[], TResult>(fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>): (...args: TArgs) => Promise<ActionResponse<TResult>>;
|
|
98
139
|
|
|
99
|
-
export { type ActionResponse, type
|
|
140
|
+
export { type ActionResponse, type MiddlewareOptions, type RouteConfig, type SupaError, createAction, withSupaAuth };
|
package/dist/server/index.js
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
// src/server/middleware/withSupaAuth.ts
|
|
2
2
|
import { createServerClient } from "@supabase/ssr";
|
|
3
3
|
import { NextResponse } from "next/server";
|
|
4
|
-
function
|
|
4
|
+
function matchPath(pattern, pathname) {
|
|
5
|
+
if (pattern.includes(":path*")) {
|
|
6
|
+
const prefix = pattern.replace(/:path\*$/, "").replace(/\/$/, "");
|
|
7
|
+
return pathname === prefix || pathname.startsWith(prefix + "/");
|
|
8
|
+
}
|
|
9
|
+
return pathname === pattern || pathname.startsWith(pattern + "/");
|
|
10
|
+
}
|
|
11
|
+
function findMatchingRoute(routes, pathname) {
|
|
12
|
+
return routes.find((route) => matchPath(route.path, pathname));
|
|
13
|
+
}
|
|
14
|
+
function extractRole(user, extractor) {
|
|
15
|
+
if (typeof extractor === "function") {
|
|
16
|
+
return extractor(user);
|
|
17
|
+
}
|
|
18
|
+
const source = extractor === "app_metadata" ? user.app_metadata : user.user_metadata;
|
|
19
|
+
const raw = source?.role;
|
|
20
|
+
if (typeof raw === "string") return raw;
|
|
21
|
+
if (Array.isArray(raw) && raw.every((r) => typeof r === "string")) {
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
function hasRequiredRole(userRole, allowedRoles) {
|
|
27
|
+
if (!allowedRoles || allowedRoles.length === 0) return true;
|
|
28
|
+
if (!userRole) return false;
|
|
29
|
+
const roles = Array.isArray(userRole) ? userRole : [userRole];
|
|
30
|
+
return roles.some((r) => allowedRoles.includes(r));
|
|
31
|
+
}
|
|
32
|
+
function withSupaAuth(options) {
|
|
5
33
|
const {
|
|
6
|
-
|
|
34
|
+
routes,
|
|
7
35
|
redirectTo = "/login",
|
|
8
|
-
|
|
36
|
+
roleExtractor = "user_metadata",
|
|
9
37
|
onAuthSuccess
|
|
10
|
-
} =
|
|
38
|
+
} = options;
|
|
11
39
|
return async function middleware(request) {
|
|
12
40
|
let response = NextResponse.next({
|
|
13
41
|
request: { headers: request.headers }
|
|
@@ -30,8 +58,8 @@ function withSupaAuth(config) {
|
|
|
30
58
|
request.cookies.set(name, value);
|
|
31
59
|
});
|
|
32
60
|
response = NextResponse.next({ request });
|
|
33
|
-
cookiesToSet.forEach(({ name, value, options }) => {
|
|
34
|
-
response.cookies.set(name, value,
|
|
61
|
+
cookiesToSet.forEach(({ name, value, options: options2 }) => {
|
|
62
|
+
response.cookies.set(name, value, options2);
|
|
35
63
|
});
|
|
36
64
|
}
|
|
37
65
|
}
|
|
@@ -40,22 +68,28 @@ function withSupaAuth(config) {
|
|
|
40
68
|
data: { user }
|
|
41
69
|
} = await supabase.auth.getUser();
|
|
42
70
|
const { pathname } = request.nextUrl;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
if (isPublicRoute) {
|
|
71
|
+
const matchedRoute = findMatchingRoute(routes, pathname);
|
|
72
|
+
if (!matchedRoute) {
|
|
47
73
|
return response;
|
|
48
74
|
}
|
|
49
|
-
|
|
50
|
-
(route) => pathname.startsWith(route)
|
|
51
|
-
);
|
|
52
|
-
if (isProtectedRoute && !user) {
|
|
75
|
+
if (!user) {
|
|
53
76
|
const loginUrl = new URL(redirectTo, request.url);
|
|
54
77
|
loginUrl.searchParams.set("next", pathname);
|
|
55
78
|
return NextResponse.redirect(loginUrl);
|
|
56
79
|
}
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
const userRole = extractRole(user, roleExtractor);
|
|
81
|
+
if (!hasRequiredRole(userRole, matchedRoute.allowedRoles)) {
|
|
82
|
+
const forbiddenUrl = new URL(redirectTo, request.url);
|
|
83
|
+
forbiddenUrl.searchParams.set("error", "forbidden");
|
|
84
|
+
forbiddenUrl.searchParams.set("next", pathname);
|
|
85
|
+
return NextResponse.redirect(forbiddenUrl);
|
|
86
|
+
}
|
|
87
|
+
if (onAuthSuccess) {
|
|
88
|
+
await onAuthSuccess({
|
|
89
|
+
id: user.id,
|
|
90
|
+
email: user.email ?? void 0,
|
|
91
|
+
role: userRole
|
|
92
|
+
});
|
|
59
93
|
}
|
|
60
94
|
return response;
|
|
61
95
|
};
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/middleware/withSupaAuth.ts","../../src/server/actions/actionWrapper.ts","../../src/shared/utils/error-handler.ts"],"sourcesContent":["import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { NextResponse, type NextRequest } from \"next/server\";\n\nimport type { SupaAuthConfig } from \"../../types\";\n\n/**\n * Create a Next.js middleware handler that protects routes based on\n * Supabase authentication state.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { withSupaAuth } from \"next-supa-utils/server\";\n *\n * export default withSupaAuth({\n * protectedRoutes: [\"/dashboard\", \"/admin\"],\n * redirectTo: \"/login\",\n * publicRoutes: [\"/admin/login\"],\n * });\n *\n * export const config = {\n * matcher: [\"/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)\"],\n * };\n * ```\n */\nexport function withSupaAuth(config: SupaAuthConfig) {\n const {\n protectedRoutes,\n redirectTo = \"/login\",\n publicRoutes = [],\n onAuthSuccess,\n } = config;\n\n return async function middleware(request: NextRequest): Promise<NextResponse> {\n // ── 1. Create a mutable response so Supabase can set cookies ──\n let response = NextResponse.next({\n request: { headers: request.headers },\n });\n\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n console.error(\n \"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n return response;\n }\n\n // ── 2. Initialize server client with middleware cookie helpers ─\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return request.cookies.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n // Forward cookies to the request so downstream server\n // components can read the updated session.\n cookiesToSet.forEach(({ name, value }) => {\n request.cookies.set(name, value);\n });\n\n // Re-create the response so it carries the updated request.\n response = NextResponse.next({ request });\n\n // Set cookies on the outgoing response so the browser\n // stores the refreshed tokens.\n cookiesToSet.forEach(({ name, value, options }) => {\n response.cookies.set(name, value, options);\n });\n },\n },\n });\n\n // ── 3. Refresh session (required to keep tokens alive) ────────\n const {\n data: { user },\n } = await supabase.auth.getUser();\n\n const { pathname } = request.nextUrl;\n\n // ── 4. Check if current path matches a public override ────────\n const isPublicRoute = publicRoutes.some((route) =>\n pathname.startsWith(route),\n );\n\n if (isPublicRoute) {\n return response;\n }\n\n // ── 5. Check if current path requires authentication ──────────\n const isProtectedRoute = protectedRoutes.some((route) =>\n pathname.startsWith(route),\n );\n\n if (isProtectedRoute && !user) {\n const loginUrl = new URL(redirectTo, request.url);\n // Preserve the originally-requested URL so the app can redirect\n // back after login.\n loginUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(loginUrl);\n }\n\n // ── 6. Optional success callback ──────────────────────────────\n if (user && onAuthSuccess) {\n await onAuthSuccess({ id: user.id, email: user.email ?? undefined });\n }\n\n return response;\n };\n}\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nimport type { ActionResponse, SupaError } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * Create a type-safe Server Action that automatically:\n * 1. Initialises a Supabase server client (with cookies)\n * 2. Wraps execution in try/catch\n * 3. Returns a standardised `{ data, error }` response\n *\n * @example\n * ```ts\n * // app/actions/profile.ts\n * \"use server\";\n * import { createAction } from \"next-supa-utils/server\";\n *\n * export const getProfile = createAction(async (supabase, userId: string) => {\n * const { data, error } = await supabase\n * .from(\"profiles\")\n * .select(\"*\")\n * .eq(\"id\", userId)\n * .single();\n *\n * if (error) throw error;\n * return data;\n * });\n *\n * // Usage in a Server Component or Client Component:\n * const result = await getProfile(\"user-uuid\");\n * if (result.error) { ... }\n * ```\n */\nexport function createAction<TArgs extends unknown[], TResult>(\n fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>,\n): (...args: TArgs) => Promise<ActionResponse<TResult>> {\n return async (...args: TArgs): Promise<ActionResponse<TResult>> => {\n try {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n return {\n data: null,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n };\n }\n\n const cookieStore = await cookies();\n\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n try {\n cookiesToSet.forEach(({ name, value, options }) => {\n cookieStore.set(name, value, options);\n });\n } catch {\n // `cookies().set()` throws when called from a Server Component.\n // In that context we only need read access — the middleware\n // handles token refresh.\n }\n },\n },\n });\n\n const data = await fn(supabase, ...args);\n\n return { data, error: null };\n } catch (caught: unknown) {\n const error: SupaError = handleSupaError(caught);\n return { data: null, error };\n }\n };\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n"],"mappings":"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","names":["createServerClient","createServerClient"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/middleware/withSupaAuth.ts","../../src/server/actions/actionWrapper.ts","../../src/shared/utils/error-handler.ts"],"sourcesContent":["import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { NextResponse, type NextRequest } from \"next/server\";\n\nimport type { MiddlewareOptions, RouteConfig } from \"../../types\";\n\n// ── Path matching utility ───────────────────────────────────────────\n\n/**\n * Converts a route pattern into a RegExp for matching.\n *\n * Supports:\n * - Exact: `\"/settings\"` → matches only `/settings`\n * - Prefix: `\"/dashboard\"` → matches `/dashboard`, `/dashboard/stats`\n * - Wildcard: `\"/admin/:path*\"` → matches `/admin`, `/admin/users`, `/admin/a/b/c`\n */\nfunction matchPath(pattern: string, pathname: string): boolean {\n // Wildcard pattern: \"/admin/:path*\" → match \"/admin\" and everything below\n if (pattern.includes(\":path*\")) {\n const prefix = pattern.replace(/:path\\*$/, \"\").replace(/\\/$/, \"\");\n return pathname === prefix || pathname.startsWith(prefix + \"/\");\n }\n\n // Prefix matching: \"/dashboard\" matches \"/dashboard/anything\"\n return pathname === pattern || pathname.startsWith(pattern + \"/\");\n}\n\n/**\n * Finds the first `RouteConfig` whose pattern matches `pathname`, or `undefined`.\n */\nfunction findMatchingRoute(\n routes: RouteConfig[],\n pathname: string,\n): RouteConfig | undefined {\n return routes.find((route) => matchPath(route.path, pathname));\n}\n\n// ── Role extraction utility ─────────────────────────────────────────\n\ntype UserMeta = {\n user_metadata: Record<string, unknown>;\n app_metadata: Record<string, unknown>;\n};\n\nfunction extractRole(\n user: UserMeta,\n extractor: MiddlewareOptions[\"roleExtractor\"],\n): string | string[] | undefined {\n if (typeof extractor === \"function\") {\n return extractor(user);\n }\n\n const source =\n extractor === \"app_metadata\" ? user.app_metadata : user.user_metadata;\n\n const raw = source?.role;\n\n if (typeof raw === \"string\") return raw;\n if (Array.isArray(raw) && raw.every((r) => typeof r === \"string\")) {\n return raw as string[];\n }\n\n return undefined;\n}\n\n/**\n * Check whether the user's role(s) satisfy the route's `allowedRoles`.\n */\nfunction hasRequiredRole(\n userRole: string | string[] | undefined,\n allowedRoles: string[] | undefined,\n): boolean {\n // No role restriction → any authenticated user is allowed.\n if (!allowedRoles || allowedRoles.length === 0) return true;\n\n if (!userRole) return false;\n\n const roles = Array.isArray(userRole) ? userRole : [userRole];\n return roles.some((r) => allowedRoles.includes(r));\n}\n\n// ── Main middleware factory ─────────────────────────────────────────\n\n/**\n * Create a Next.js middleware handler that protects routes with\n * authentication **and** optional role-based access control (RBAC).\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { withSupaAuth } from \"next-supa-utils/server\";\n *\n * export default withSupaAuth({\n * routes: [\n * { path: \"/dashboard\" }, // any authed user\n * { path: \"/admin/:path*\", allowedRoles: [\"admin\"] }, // admin only\n * { path: \"/editor/:path*\", allowedRoles: [\"admin\", \"editor\"] },\n * ],\n * redirectTo: \"/login\",\n * roleExtractor: \"user_metadata\", // or \"app_metadata\" or a custom fn\n * });\n *\n * export const config = {\n * matcher: [\"/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)\"],\n * };\n * ```\n */\nexport function withSupaAuth(options: MiddlewareOptions) {\n const {\n routes,\n redirectTo = \"/login\",\n roleExtractor = \"user_metadata\",\n onAuthSuccess,\n } = options;\n\n return async function middleware(request: NextRequest): Promise<NextResponse> {\n // ── 1. Create a mutable response so Supabase can set cookies ──\n let response = NextResponse.next({\n request: { headers: request.headers },\n });\n\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n console.error(\n \"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n return response;\n }\n\n // ── 2. Initialize server client with middleware cookie helpers ─\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return request.cookies.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n cookiesToSet.forEach(({ name, value }) => {\n request.cookies.set(name, value);\n });\n\n response = NextResponse.next({ request });\n\n cookiesToSet.forEach(({ name, value, options }) => {\n response.cookies.set(name, value, options);\n });\n },\n },\n });\n\n // ── 3. Refresh session (required to keep tokens alive) ────────\n const {\n data: { user },\n } = await supabase.auth.getUser();\n\n const { pathname } = request.nextUrl;\n\n // ── 4. Find the matching route config ─────────────────────────\n const matchedRoute = findMatchingRoute(routes, pathname);\n\n // No matching route → not a protected path, pass through.\n if (!matchedRoute) {\n return response;\n }\n\n // ── 5. Check authentication ───────────────────────────────────\n if (!user) {\n const loginUrl = new URL(redirectTo, request.url);\n loginUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(loginUrl);\n }\n\n // ── 6. Check role-based access ────────────────────────────────\n const userRole = extractRole(user, roleExtractor);\n\n if (!hasRequiredRole(userRole, matchedRoute.allowedRoles)) {\n // User is logged in but lacks the required role.\n const forbiddenUrl = new URL(redirectTo, request.url);\n forbiddenUrl.searchParams.set(\"error\", \"forbidden\");\n forbiddenUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(forbiddenUrl);\n }\n\n // ── 7. Success callback ───────────────────────────────────────\n if (onAuthSuccess) {\n await onAuthSuccess({\n id: user.id,\n email: user.email ?? undefined,\n role: userRole,\n });\n }\n\n return response;\n };\n}\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nimport type { ActionResponse, SupaError } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * Create a type-safe Server Action that automatically:\n * 1. Initialises a Supabase server client (with cookies)\n * 2. Wraps execution in try/catch\n * 3. Returns a standardised `{ data, error }` response\n *\n * @example\n * ```ts\n * // app/actions/profile.ts\n * \"use server\";\n * import { createAction } from \"next-supa-utils/server\";\n *\n * export const getProfile = createAction(async (supabase, userId: string) => {\n * const { data, error } = await supabase\n * .from(\"profiles\")\n * .select(\"*\")\n * .eq(\"id\", userId)\n * .single();\n *\n * if (error) throw error;\n * return data;\n * });\n *\n * // Usage in a Server Component or Client Component:\n * const result = await getProfile(\"user-uuid\");\n * if (result.error) { ... }\n * ```\n */\nexport function createAction<TArgs extends unknown[], TResult>(\n fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>,\n): (...args: TArgs) => Promise<ActionResponse<TResult>> {\n return async (...args: TArgs): Promise<ActionResponse<TResult>> => {\n try {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n return {\n data: null,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n };\n }\n\n const cookieStore = await cookies();\n\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n try {\n cookiesToSet.forEach(({ name, value, options }) => {\n cookieStore.set(name, value, options);\n });\n } catch {\n // `cookies().set()` throws when called from a Server Component.\n // In that context we only need read access — the middleware\n // handles token refresh.\n }\n },\n },\n });\n\n const data = await fn(supabase, ...args);\n\n return { data, error: null };\n } catch (caught: unknown) {\n const error: SupaError = handleSupaError(caught);\n return { data: null, error };\n }\n };\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n"],"mappings":"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","names":["options","createServerClient","createServerClient"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shared/index.ts","../../src/shared/utils/error-handler.ts"],"sourcesContent":["// ── Shared entry point ──────────────────────────────────────────────\n// Re-exports types and utilities used across client & server modules.\n\nexport { handleSupaError } from \"./utils/error-handler\";\n\nexport type {\n SupaError,\n ActionResponse,\n
|
|
1
|
+
{"version":3,"sources":["../../src/shared/index.ts","../../src/shared/utils/error-handler.ts"],"sourcesContent":["// ── Shared entry point ──────────────────────────────────────────────\n// Re-exports types and utilities used across client & server modules.\n\nexport { handleSupaError } from \"./utils/error-handler\";\n\nexport type {\n SupaError,\n ActionResponse,\n MiddlewareOptions,\n RouteConfig,\n UseSupaUserReturn,\n UseSupaSessionReturn,\n} from \"../types\";\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWO,SAAS,gBAAgB,OAA2B;AAEzD,MAAI,iBAAiB,OAAO;AAC1B,UAAM,SAAS;AACf,WAAO;AAAA,MACL,SAAS,MAAM;AAAA,MACf,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAAA,MACtD,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,IAC9D;AAAA,EACF;AAGA,MACE,OAAO,UAAU,YACjB,UAAU,QACV,aAAa,SACb,OAAQ,MAAkC,YAAY,UACtD;AACA,UAAM,MAAM;AACZ,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAAA,MAChD,QAAQ,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS;AAAA,IACxD;AAAA,EACF;AAGA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAGA,SAAO,EAAE,SAAS,4BAA4B;AAChD;","names":[]}
|
package/dist/shared/index.d.cts
CHANGED
|
@@ -14,32 +14,69 @@ type ActionResponse<T> = {
|
|
|
14
14
|
data: null;
|
|
15
15
|
error: SupaError;
|
|
16
16
|
};
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Defines a protected route with optional role-based access control.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* { path: "/admin/:path*", allowedRoles: ["admin", "super_admin"] }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
interface RouteConfig {
|
|
18
26
|
/**
|
|
19
|
-
* Route
|
|
20
|
-
* Supports simple prefix matching.
|
|
27
|
+
* Route pattern to match.
|
|
21
28
|
*
|
|
22
|
-
*
|
|
29
|
+
* Supports:
|
|
30
|
+
* - Exact paths: `"/settings"`
|
|
31
|
+
* - Prefix matching: `"/dashboard"` matches `/dashboard/anything`
|
|
32
|
+
* - Wildcards: `"/admin/:path*"` matches `/admin/users`, `/admin/settings/edit`, etc.
|
|
23
33
|
*/
|
|
24
|
-
|
|
34
|
+
path: string;
|
|
25
35
|
/**
|
|
26
|
-
*
|
|
36
|
+
* Roles that are allowed to access this route.
|
|
37
|
+
*
|
|
38
|
+
* - If **omitted or empty**, any *authenticated* user can access the route.
|
|
39
|
+
* - If **provided**, only users whose role is in this list are allowed.
|
|
40
|
+
*
|
|
41
|
+
* @example ["admin", "editor"]
|
|
42
|
+
*/
|
|
43
|
+
allowedRoles?: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Configuration for the `withSupaAuth` middleware.
|
|
47
|
+
*/
|
|
48
|
+
interface MiddlewareOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Protected route definitions. Each entry can optionally restrict access
|
|
51
|
+
* to specific roles via `allowedRoles`.
|
|
52
|
+
*/
|
|
53
|
+
routes: RouteConfig[];
|
|
54
|
+
/**
|
|
55
|
+
* Where to redirect unauthenticated or unauthorized users.
|
|
27
56
|
* @default "/login"
|
|
28
57
|
*/
|
|
29
58
|
redirectTo?: string;
|
|
30
59
|
/**
|
|
31
|
-
*
|
|
60
|
+
* Strategy for extracting the user's role from the Supabase user object.
|
|
61
|
+
*
|
|
62
|
+
* - `"user_metadata"` — reads `user.user_metadata.role`
|
|
63
|
+
* - `"app_metadata"` — reads `user.app_metadata.role`
|
|
64
|
+
* - A custom function for advanced scenarios (e.g. multiple roles, JWT claims).
|
|
32
65
|
*
|
|
33
|
-
* @
|
|
66
|
+
* @default "user_metadata"
|
|
34
67
|
*/
|
|
35
|
-
|
|
68
|
+
roleExtractor?: "user_metadata" | "app_metadata" | ((user: {
|
|
69
|
+
user_metadata: Record<string, unknown>;
|
|
70
|
+
app_metadata: Record<string, unknown>;
|
|
71
|
+
}) => string | string[] | undefined);
|
|
36
72
|
/**
|
|
37
|
-
* Optional callback invoked
|
|
38
|
-
*
|
|
73
|
+
* Optional callback invoked when a user is authenticated and authorized.
|
|
74
|
+
* Useful for logging, analytics, or injecting custom response headers.
|
|
39
75
|
*/
|
|
40
76
|
onAuthSuccess?: (user: {
|
|
41
77
|
id: string;
|
|
42
78
|
email?: string;
|
|
79
|
+
role?: string | string[];
|
|
43
80
|
}) => void | Promise<void>;
|
|
44
81
|
}
|
|
45
82
|
interface UseSupaUserReturn {
|
|
@@ -64,4 +101,4 @@ interface UseSupaSessionReturn {
|
|
|
64
101
|
*/
|
|
65
102
|
declare function handleSupaError(error: unknown): SupaError;
|
|
66
103
|
|
|
67
|
-
export { type ActionResponse, type
|
|
104
|
+
export { type ActionResponse, type MiddlewareOptions, type RouteConfig, type SupaError, type UseSupaSessionReturn, type UseSupaUserReturn, handleSupaError };
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -14,32 +14,69 @@ type ActionResponse<T> = {
|
|
|
14
14
|
data: null;
|
|
15
15
|
error: SupaError;
|
|
16
16
|
};
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Defines a protected route with optional role-based access control.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* { path: "/admin/:path*", allowedRoles: ["admin", "super_admin"] }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
interface RouteConfig {
|
|
18
26
|
/**
|
|
19
|
-
* Route
|
|
20
|
-
* Supports simple prefix matching.
|
|
27
|
+
* Route pattern to match.
|
|
21
28
|
*
|
|
22
|
-
*
|
|
29
|
+
* Supports:
|
|
30
|
+
* - Exact paths: `"/settings"`
|
|
31
|
+
* - Prefix matching: `"/dashboard"` matches `/dashboard/anything`
|
|
32
|
+
* - Wildcards: `"/admin/:path*"` matches `/admin/users`, `/admin/settings/edit`, etc.
|
|
23
33
|
*/
|
|
24
|
-
|
|
34
|
+
path: string;
|
|
25
35
|
/**
|
|
26
|
-
*
|
|
36
|
+
* Roles that are allowed to access this route.
|
|
37
|
+
*
|
|
38
|
+
* - If **omitted or empty**, any *authenticated* user can access the route.
|
|
39
|
+
* - If **provided**, only users whose role is in this list are allowed.
|
|
40
|
+
*
|
|
41
|
+
* @example ["admin", "editor"]
|
|
42
|
+
*/
|
|
43
|
+
allowedRoles?: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Configuration for the `withSupaAuth` middleware.
|
|
47
|
+
*/
|
|
48
|
+
interface MiddlewareOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Protected route definitions. Each entry can optionally restrict access
|
|
51
|
+
* to specific roles via `allowedRoles`.
|
|
52
|
+
*/
|
|
53
|
+
routes: RouteConfig[];
|
|
54
|
+
/**
|
|
55
|
+
* Where to redirect unauthenticated or unauthorized users.
|
|
27
56
|
* @default "/login"
|
|
28
57
|
*/
|
|
29
58
|
redirectTo?: string;
|
|
30
59
|
/**
|
|
31
|
-
*
|
|
60
|
+
* Strategy for extracting the user's role from the Supabase user object.
|
|
61
|
+
*
|
|
62
|
+
* - `"user_metadata"` — reads `user.user_metadata.role`
|
|
63
|
+
* - `"app_metadata"` — reads `user.app_metadata.role`
|
|
64
|
+
* - A custom function for advanced scenarios (e.g. multiple roles, JWT claims).
|
|
32
65
|
*
|
|
33
|
-
* @
|
|
66
|
+
* @default "user_metadata"
|
|
34
67
|
*/
|
|
35
|
-
|
|
68
|
+
roleExtractor?: "user_metadata" | "app_metadata" | ((user: {
|
|
69
|
+
user_metadata: Record<string, unknown>;
|
|
70
|
+
app_metadata: Record<string, unknown>;
|
|
71
|
+
}) => string | string[] | undefined);
|
|
36
72
|
/**
|
|
37
|
-
* Optional callback invoked
|
|
38
|
-
*
|
|
73
|
+
* Optional callback invoked when a user is authenticated and authorized.
|
|
74
|
+
* Useful for logging, analytics, or injecting custom response headers.
|
|
39
75
|
*/
|
|
40
76
|
onAuthSuccess?: (user: {
|
|
41
77
|
id: string;
|
|
42
78
|
email?: string;
|
|
79
|
+
role?: string | string[];
|
|
43
80
|
}) => void | Promise<void>;
|
|
44
81
|
}
|
|
45
82
|
interface UseSupaUserReturn {
|
|
@@ -64,4 +101,4 @@ interface UseSupaSessionReturn {
|
|
|
64
101
|
*/
|
|
65
102
|
declare function handleSupaError(error: unknown): SupaError;
|
|
66
103
|
|
|
67
|
-
export { type ActionResponse, type
|
|
104
|
+
export { type ActionResponse, type MiddlewareOptions, type RouteConfig, type SupaError, type UseSupaSessionReturn, type UseSupaUserReturn, handleSupaError };
|
package/package.json
CHANGED