next-supa-utils 0.1.0
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 +326 -0
- package/dist/client/index.cjs +154 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +68 -0
- package/dist/client/index.d.ts +68 -0
- package/dist/client/index.js +127 -0
- package/dist/client/index.js.map +1 -0
- package/dist/server/index.cjs +163 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +99 -0
- package/dist/server/index.d.ts +99 -0
- package/dist/server/index.js +135 -0
- package/dist/server/index.js.map +1 -0
- package/dist/shared/index.cjs +54 -0
- package/dist/shared/index.cjs.map +1 -0
- package/dist/shared/index.d.cts +67 -0
- package/dist/shared/index.d.ts +67 -0
- package/dist/shared/index.js +27 -0
- package/dist/shared/index.js.map +1 -0
- package/package.json +99 -0
package/README.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">next-supa-utils</h1>
|
|
3
|
+
<p align="center">
|
|
4
|
+
Eliminate Supabase boilerplate in Next.js App Router.<br/>
|
|
5
|
+
Hooks, middleware helpers, and server action wrappers — all type-safe.
|
|
6
|
+
</p>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="#installation">Installation</a> •
|
|
11
|
+
<a href="#quick-start">Quick Start</a> •
|
|
12
|
+
<a href="#api-reference">API Reference</a> •
|
|
13
|
+
<a href="#contributing">Contributing</a> •
|
|
14
|
+
<a href="#license">License</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why?
|
|
20
|
+
|
|
21
|
+
Every Next.js + Supabase project ends up with the same boilerplate:
|
|
22
|
+
|
|
23
|
+
- Creating Supabase clients with cookie handling for middleware, server components, and client components
|
|
24
|
+
- Writing try/catch wrappers around every server action
|
|
25
|
+
- Manually checking auth state in middleware and redirecting
|
|
26
|
+
|
|
27
|
+
**next-supa-utils** extracts these patterns into a single, type-safe library with separate entry points for client and server code — fully compatible with the Next.js App Router architecture.
|
|
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
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install next-supa-utils
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Peer Dependencies
|
|
45
|
+
|
|
46
|
+
Make sure you have the following installed in your project:
|
|
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
|
|
60
|
+
|
|
61
|
+
Add these to your `.env.local`:
|
|
62
|
+
|
|
63
|
+
```env
|
|
64
|
+
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
|
65
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both variables are required. All helpers in this library read from these environment variables automatically.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
### 1. Protect Routes with Middleware
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// middleware.ts
|
|
78
|
+
import { withSupaAuth } from "next-supa-utils/server";
|
|
79
|
+
|
|
80
|
+
export default withSupaAuth({
|
|
81
|
+
protectedRoutes: ["/dashboard", "/admin", "/settings"],
|
|
82
|
+
redirectTo: "/login",
|
|
83
|
+
publicRoutes: ["/admin/login"],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const config = {
|
|
87
|
+
matcher: [
|
|
88
|
+
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. Create Type-Safe Server Actions
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// app/actions/profile.ts
|
|
97
|
+
"use server";
|
|
98
|
+
import { createAction } from "next-supa-utils/server";
|
|
99
|
+
|
|
100
|
+
export const getProfile = createAction(async (supabase, userId: string) => {
|
|
101
|
+
const { data, error } = await supabase
|
|
102
|
+
.from("profiles")
|
|
103
|
+
.select("*")
|
|
104
|
+
.eq("id", userId)
|
|
105
|
+
.single();
|
|
106
|
+
|
|
107
|
+
if (error) throw error;
|
|
108
|
+
return data;
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// Usage in any component
|
|
114
|
+
const result = await getProfile("user-uuid");
|
|
115
|
+
|
|
116
|
+
if (result.error) {
|
|
117
|
+
console.error(result.error.message);
|
|
118
|
+
} else {
|
|
119
|
+
console.log(result.data);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 3. Use Auth State in Client Components
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
"use client";
|
|
127
|
+
import { useSupaUser } from "next-supa-utils/client";
|
|
128
|
+
|
|
129
|
+
export default function Avatar() {
|
|
130
|
+
const { user, loading, error } = useSupaUser();
|
|
131
|
+
|
|
132
|
+
if (loading) return <p>Loading…</p>;
|
|
133
|
+
if (error) return <p>Error: {error.message}</p>;
|
|
134
|
+
if (!user) return <p>Not signed in</p>;
|
|
135
|
+
|
|
136
|
+
return <p>Hello, {user.email}!</p>;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## API Reference
|
|
143
|
+
|
|
144
|
+
### Server — `next-supa-utils/server`
|
|
145
|
+
|
|
146
|
+
#### `withSupaAuth(config)`
|
|
147
|
+
|
|
148
|
+
Creates a Next.js middleware function that handles session refresh and route protection.
|
|
149
|
+
|
|
150
|
+
**Parameters:**
|
|
151
|
+
|
|
152
|
+
| Property | Type | Required | Default | Description |
|
|
153
|
+
|---|---|---|---|---|
|
|
154
|
+
| `protectedRoutes` | `string[]` | ✅ | — | Route prefixes that require authentication |
|
|
155
|
+
| `redirectTo` | `string` | — | `"/login"` | Where to redirect unauthenticated users |
|
|
156
|
+
| `publicRoutes` | `string[]` | — | `[]` | Routes that are always public, even if matching a protected prefix |
|
|
157
|
+
| `onAuthSuccess` | `(user: { id: string; email?: string }) => void \| Promise<void>` | — | — | Optional callback after successful auth verification |
|
|
158
|
+
|
|
159
|
+
**Returns:** `(request: NextRequest) => Promise<NextResponse>`
|
|
160
|
+
|
|
161
|
+
**Behavior:**
|
|
162
|
+
1. Creates a Supabase server client with proper cookie forwarding
|
|
163
|
+
2. Calls `supabase.auth.getUser()` to refresh the session
|
|
164
|
+
3. If the current path matches `publicRoutes`, allows access immediately
|
|
165
|
+
4. If the current path matches `protectedRoutes` and the user is not authenticated, redirects to `redirectTo` with a `?next=<original_path>` query parameter
|
|
166
|
+
5. Calls `onAuthSuccess` if the user is authenticated and the callback is provided
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
#### `createAction(fn)`
|
|
171
|
+
|
|
172
|
+
Wraps an async function into a server action with automatic Supabase client initialization and error handling.
|
|
173
|
+
|
|
174
|
+
**Signature:**
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
function createAction<TArgs extends unknown[], TResult>(
|
|
178
|
+
fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>
|
|
179
|
+
): (...args: TArgs) => Promise<ActionResponse<TResult>>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Returns:** `ActionResponse<TResult>` — a discriminated union:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// On success:
|
|
186
|
+
{ data: TResult; error: null }
|
|
187
|
+
|
|
188
|
+
// On failure:
|
|
189
|
+
{ data: null; error: SupaError }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Behavior:**
|
|
193
|
+
1. Creates a Supabase server client using `cookies()` from `next/headers`
|
|
194
|
+
2. Passes the client as the first argument to your function
|
|
195
|
+
3. Wraps execution in try/catch — any thrown error is normalized into a `SupaError`
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Client — `next-supa-utils/client`
|
|
200
|
+
|
|
201
|
+
> ⚠️ All client exports include the `"use client"` directive. They must be used inside Client Components only.
|
|
202
|
+
|
|
203
|
+
#### `useSupaUser()`
|
|
204
|
+
|
|
205
|
+
React hook that provides the current authenticated user and subscribes to auth state changes.
|
|
206
|
+
|
|
207
|
+
**Returns:** `UseSupaUserReturn`
|
|
208
|
+
|
|
209
|
+
| Property | Type | Description |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `user` | `User \| null` | The current Supabase user object, or `null` if not authenticated |
|
|
212
|
+
| `loading` | `boolean` | `true` while the initial fetch is in progress |
|
|
213
|
+
| `error` | `SupaError \| null` | Error details if the fetch failed |
|
|
214
|
+
|
|
215
|
+
**Behavior:**
|
|
216
|
+
1. Creates a browser client via `createBrowserClient` from `@supabase/ssr`
|
|
217
|
+
2. Calls `supabase.auth.getUser()` on mount
|
|
218
|
+
3. Subscribes to `onAuthStateChange` for real-time updates (sign in, sign out, token refresh)
|
|
219
|
+
4. Cleans up the subscription on unmount
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
#### `useSupaSession()`
|
|
224
|
+
|
|
225
|
+
React hook that provides the current session (access token, refresh token, expiry) and subscribes to auth state changes.
|
|
226
|
+
|
|
227
|
+
**Returns:** `UseSupaSessionReturn`
|
|
228
|
+
|
|
229
|
+
| Property | Type | Description |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `session` | `Session \| null` | The current Supabase session, or `null` if not authenticated |
|
|
232
|
+
| `loading` | `boolean` | `true` while the initial fetch is in progress |
|
|
233
|
+
| `error` | `SupaError \| null` | Error details if the fetch failed |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### Shared — `next-supa-utils`
|
|
238
|
+
|
|
239
|
+
#### `handleSupaError(error)`
|
|
240
|
+
|
|
241
|
+
Normalizes any thrown value into a consistent `SupaError` shape. Used internally by `createAction` and the hooks, but also exported for direct use.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
function handleSupaError(error: unknown): SupaError
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Handles:**
|
|
248
|
+
- Supabase `AuthError` / `PostgrestError` (extracts `message`, `code`, `status`)
|
|
249
|
+
- Standard `Error` instances
|
|
250
|
+
- Plain objects with a `message` property
|
|
251
|
+
- Strings
|
|
252
|
+
- Unknown values (fallback: `"An unknown error occurred"`)
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### Types
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
interface SupaError {
|
|
260
|
+
message: string;
|
|
261
|
+
code?: string;
|
|
262
|
+
status?: number;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type ActionResponse<T> =
|
|
266
|
+
| { data: T; error: null }
|
|
267
|
+
| { data: null; error: SupaError };
|
|
268
|
+
|
|
269
|
+
interface SupaAuthConfig {
|
|
270
|
+
protectedRoutes: string[];
|
|
271
|
+
redirectTo?: string;
|
|
272
|
+
publicRoutes?: string[];
|
|
273
|
+
onAuthSuccess?: (user: { id: string; email?: string }) => void | Promise<void>;
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Project Structure
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
src/
|
|
283
|
+
├── client/ # "use client" — browser-only code
|
|
284
|
+
│ ├── hooks/
|
|
285
|
+
│ │ ├── useSupaUser.ts
|
|
286
|
+
│ │ └── useSupaSession.ts
|
|
287
|
+
│ └── index.ts
|
|
288
|
+
├── server/ # Server-only (Node/Edge runtime)
|
|
289
|
+
│ ├── middleware/
|
|
290
|
+
│ │ └── withSupaAuth.ts
|
|
291
|
+
│ ├── actions/
|
|
292
|
+
│ │ └── actionWrapper.ts
|
|
293
|
+
│ └── index.ts
|
|
294
|
+
├── shared/ # Isomorphic utilities
|
|
295
|
+
│ ├── utils/
|
|
296
|
+
│ │ └── error-handler.ts
|
|
297
|
+
│ └── index.ts
|
|
298
|
+
└── types/
|
|
299
|
+
└── index.ts
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Import Paths
|
|
303
|
+
|
|
304
|
+
| Import | Environment | Contains |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| `next-supa-utils/client` | Client Components | `useSupaUser`, `useSupaSession` |
|
|
307
|
+
| `next-supa-utils/server` | Server Components, Middleware, Server Actions | `withSupaAuth`, `createAction` |
|
|
308
|
+
| `next-supa-utils` | Anywhere | `handleSupaError`, all types |
|
|
309
|
+
|
|
310
|
+
## Contributing
|
|
311
|
+
|
|
312
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
313
|
+
|
|
314
|
+
1. Fork the repository
|
|
315
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
316
|
+
3. Install dependencies (`npm install`)
|
|
317
|
+
4. Make your changes
|
|
318
|
+
5. Run the type checker (`npm run typecheck`)
|
|
319
|
+
6. Build the project (`npm run build`)
|
|
320
|
+
7. Commit your changes (`git commit -m 'feat: add amazing feature'`)
|
|
321
|
+
8. Push to the branch (`git push origin feature/amazing-feature`)
|
|
322
|
+
9. Open a Pull Request
|
|
323
|
+
|
|
324
|
+
## License
|
|
325
|
+
|
|
326
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/client/index.ts
|
|
23
|
+
var client_exports = {};
|
|
24
|
+
__export(client_exports, {
|
|
25
|
+
useSupaSession: () => useSupaSession,
|
|
26
|
+
useSupaUser: () => useSupaUser
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(client_exports);
|
|
29
|
+
|
|
30
|
+
// src/client/hooks/useSupaUser.ts
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
var import_ssr = require("@supabase/ssr");
|
|
33
|
+
|
|
34
|
+
// src/shared/utils/error-handler.ts
|
|
35
|
+
function handleSupaError(error) {
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
const record = error;
|
|
38
|
+
return {
|
|
39
|
+
message: error.message,
|
|
40
|
+
code: typeof record.code === "string" ? record.code : void 0,
|
|
41
|
+
status: typeof record.status === "number" ? record.status : void 0
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
45
|
+
const err = error;
|
|
46
|
+
return {
|
|
47
|
+
message: err.message,
|
|
48
|
+
code: typeof err.code === "string" ? err.code : void 0,
|
|
49
|
+
status: typeof err.status === "number" ? err.status : void 0
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (typeof error === "string") {
|
|
53
|
+
return { message: error };
|
|
54
|
+
}
|
|
55
|
+
return { message: "An unknown error occurred" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/client/hooks/useSupaUser.ts
|
|
59
|
+
function useSupaUser() {
|
|
60
|
+
const [state, setState] = (0, import_react.useState)({
|
|
61
|
+
user: null,
|
|
62
|
+
loading: true,
|
|
63
|
+
error: null
|
|
64
|
+
});
|
|
65
|
+
(0, import_react.useEffect)(() => {
|
|
66
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
67
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
68
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
69
|
+
setState({
|
|
70
|
+
user: null,
|
|
71
|
+
loading: false,
|
|
72
|
+
error: {
|
|
73
|
+
message: "Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.",
|
|
74
|
+
code: "CONFIG_ERROR"
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const supabase = (0, import_ssr.createBrowserClient)(supabaseUrl, supabaseAnonKey);
|
|
80
|
+
supabase.auth.getUser().then(({ data, error }) => {
|
|
81
|
+
setState({
|
|
82
|
+
user: data.user,
|
|
83
|
+
loading: false,
|
|
84
|
+
error: error ? handleSupaError(error) : null
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
const {
|
|
88
|
+
data: { subscription }
|
|
89
|
+
} = supabase.auth.onAuthStateChange((_event, session) => {
|
|
90
|
+
setState((prev) => ({
|
|
91
|
+
...prev,
|
|
92
|
+
user: session?.user ?? null,
|
|
93
|
+
loading: false
|
|
94
|
+
}));
|
|
95
|
+
});
|
|
96
|
+
return () => {
|
|
97
|
+
subscription.unsubscribe();
|
|
98
|
+
};
|
|
99
|
+
}, []);
|
|
100
|
+
return state;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/client/hooks/useSupaSession.ts
|
|
104
|
+
var import_react2 = require("react");
|
|
105
|
+
var import_ssr2 = require("@supabase/ssr");
|
|
106
|
+
function useSupaSession() {
|
|
107
|
+
const [state, setState] = (0, import_react2.useState)({
|
|
108
|
+
session: null,
|
|
109
|
+
loading: true,
|
|
110
|
+
error: null
|
|
111
|
+
});
|
|
112
|
+
(0, import_react2.useEffect)(() => {
|
|
113
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
114
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
115
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
116
|
+
setState({
|
|
117
|
+
session: null,
|
|
118
|
+
loading: false,
|
|
119
|
+
error: {
|
|
120
|
+
message: "Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.",
|
|
121
|
+
code: "CONFIG_ERROR"
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const supabase = (0, import_ssr2.createBrowserClient)(supabaseUrl, supabaseAnonKey);
|
|
127
|
+
supabase.auth.getSession().then(({ data, error }) => {
|
|
128
|
+
setState({
|
|
129
|
+
session: data.session,
|
|
130
|
+
loading: false,
|
|
131
|
+
error: error ? handleSupaError(error) : null
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
const {
|
|
135
|
+
data: { subscription }
|
|
136
|
+
} = supabase.auth.onAuthStateChange((_event, session) => {
|
|
137
|
+
setState((prev) => ({
|
|
138
|
+
...prev,
|
|
139
|
+
session,
|
|
140
|
+
loading: false
|
|
141
|
+
}));
|
|
142
|
+
});
|
|
143
|
+
return () => {
|
|
144
|
+
subscription.unsubscribe();
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
return state;
|
|
148
|
+
}
|
|
149
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
150
|
+
0 && (module.exports = {
|
|
151
|
+
useSupaSession,
|
|
152
|
+
useSupaUser
|
|
153
|
+
});
|
|
154
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts","../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts"],"sourcesContent":["\"use client\";\n\n// ── Client entry point ──────────────────────────────────────────────\n// This module MUST only be imported in Client Components.\n// The \"use client\" directive ensures Next.js treats the entire\n// sub-tree as client-side code.\n\nexport { useSupaUser } from \"./hooks/useSupaUser\";\nexport { useSupaSession } from \"./hooks/useSupaSession\";\n\n// Re-export types consumers commonly need alongside client helpers.\nexport type {\n UseSupaUserReturn,\n UseSupaSessionReturn,\n SupaError,\n} from \"../types\";\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\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 setState({\n user: null,\n loading: false,\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 return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\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","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\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 setState({\n session: null,\n loading: false,\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 return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n"],"mappings":"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","names":["import_react","import_ssr"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/** Standardized error shape returned by all next-supa-utils helpers. */
|
|
4
|
+
interface SupaError {
|
|
5
|
+
message: string;
|
|
6
|
+
code?: string;
|
|
7
|
+
status?: number;
|
|
8
|
+
}
|
|
9
|
+
interface UseSupaUserReturn {
|
|
10
|
+
user: Awaited<ReturnType<SupabaseClient["auth"]["getUser"]>>["data"]["user"];
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: SupaError | null;
|
|
13
|
+
}
|
|
14
|
+
interface UseSupaSessionReturn {
|
|
15
|
+
session: Awaited<ReturnType<SupabaseClient["auth"]["getSession"]>>["data"]["session"];
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: SupaError | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* React hook that provides the current Supabase user and
|
|
22
|
+
* subscribes to real-time auth state changes.
|
|
23
|
+
*
|
|
24
|
+
* Must be used inside a Client Component (`"use client"`).
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* "use client";
|
|
29
|
+
* import { useSupaUser } from "next-supa-utils/client";
|
|
30
|
+
*
|
|
31
|
+
* export default function Avatar() {
|
|
32
|
+
* const { user, loading, error } = useSupaUser();
|
|
33
|
+
*
|
|
34
|
+
* if (loading) return <p>Loading…</p>;
|
|
35
|
+
* if (error) return <p>Error: {error.message}</p>;
|
|
36
|
+
* if (!user) return <p>Not signed in</p>;
|
|
37
|
+
*
|
|
38
|
+
* return <p>Hello, {user.email}</p>;
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function useSupaUser(): UseSupaUserReturn;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* React hook that provides the current Supabase session and
|
|
46
|
+
* subscribes to real-time auth state changes.
|
|
47
|
+
*
|
|
48
|
+
* Must be used inside a Client Component (`"use client"`).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* "use client";
|
|
53
|
+
* import { useSupaSession } from "next-supa-utils/client";
|
|
54
|
+
*
|
|
55
|
+
* export default function TokenDisplay() {
|
|
56
|
+
* const { session, loading, error } = useSupaSession();
|
|
57
|
+
*
|
|
58
|
+
* if (loading) return <p>Loading…</p>;
|
|
59
|
+
* if (error) return <p>Error: {error.message}</p>;
|
|
60
|
+
* if (!session) return <p>No active session</p>;
|
|
61
|
+
*
|
|
62
|
+
* return <p>Token expires at: {session.expires_at}</p>;
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function useSupaSession(): UseSupaSessionReturn;
|
|
67
|
+
|
|
68
|
+
export { type SupaError, type UseSupaSessionReturn, type UseSupaUserReturn, useSupaSession, useSupaUser };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/** Standardized error shape returned by all next-supa-utils helpers. */
|
|
4
|
+
interface SupaError {
|
|
5
|
+
message: string;
|
|
6
|
+
code?: string;
|
|
7
|
+
status?: number;
|
|
8
|
+
}
|
|
9
|
+
interface UseSupaUserReturn {
|
|
10
|
+
user: Awaited<ReturnType<SupabaseClient["auth"]["getUser"]>>["data"]["user"];
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: SupaError | null;
|
|
13
|
+
}
|
|
14
|
+
interface UseSupaSessionReturn {
|
|
15
|
+
session: Awaited<ReturnType<SupabaseClient["auth"]["getSession"]>>["data"]["session"];
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: SupaError | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* React hook that provides the current Supabase user and
|
|
22
|
+
* subscribes to real-time auth state changes.
|
|
23
|
+
*
|
|
24
|
+
* Must be used inside a Client Component (`"use client"`).
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* "use client";
|
|
29
|
+
* import { useSupaUser } from "next-supa-utils/client";
|
|
30
|
+
*
|
|
31
|
+
* export default function Avatar() {
|
|
32
|
+
* const { user, loading, error } = useSupaUser();
|
|
33
|
+
*
|
|
34
|
+
* if (loading) return <p>Loading…</p>;
|
|
35
|
+
* if (error) return <p>Error: {error.message}</p>;
|
|
36
|
+
* if (!user) return <p>Not signed in</p>;
|
|
37
|
+
*
|
|
38
|
+
* return <p>Hello, {user.email}</p>;
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function useSupaUser(): UseSupaUserReturn;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* React hook that provides the current Supabase session and
|
|
46
|
+
* subscribes to real-time auth state changes.
|
|
47
|
+
*
|
|
48
|
+
* Must be used inside a Client Component (`"use client"`).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* "use client";
|
|
53
|
+
* import { useSupaSession } from "next-supa-utils/client";
|
|
54
|
+
*
|
|
55
|
+
* export default function TokenDisplay() {
|
|
56
|
+
* const { session, loading, error } = useSupaSession();
|
|
57
|
+
*
|
|
58
|
+
* if (loading) return <p>Loading…</p>;
|
|
59
|
+
* if (error) return <p>Error: {error.message}</p>;
|
|
60
|
+
* if (!session) return <p>No active session</p>;
|
|
61
|
+
*
|
|
62
|
+
* return <p>Token expires at: {session.expires_at}</p>;
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function useSupaSession(): UseSupaSessionReturn;
|
|
67
|
+
|
|
68
|
+
export { type SupaError, type UseSupaSessionReturn, type UseSupaUserReturn, useSupaSession, useSupaUser };
|