@auth-ezz/nextjs 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 +418 -0
- package/bin/index.js +67 -0
- package/client/AuthProvider.js +18 -0
- package/client/SignInButton.js +28 -0
- package/client/index.d.ts +16 -0
- package/client/index.js +7 -0
- package/client/signOut.js +8 -0
- package/client/useUser.js +6 -0
- package/client/userButton.js +65 -0
- package/index.d.ts +5 -0
- package/middleware/authMiddleware.d.ts +3 -0
- package/middleware/authMiddleware.js +14 -0
- package/package.json +31 -0
- package/server/auth.js +18 -0
- package/server/currentUser.js +6 -0
- package/server/handleAuthCallback.js +50 -0
- package/server/handleLogout.js +5 -0
- package/server/index.d.ts +24 -0
- package/server/index.js +6 -0
- package/server/logout.js +25 -0
- package/server/requireAuth.js +34 -0
- package/templates/app/api/auth/callback/route.ts +5 -0
- package/templates/app/api/auth/logout/route.ts +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# @ezz-auth/next
|
|
2
|
+
|
|
3
|
+
A secure, high-performance authentication SDK designed specifically for modern Next.js applications using the App Router. Built on HTTP-only cookies and server-side session validation for maximum security.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/%40ezz-auth%2Fnext)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## ✨ Features
|
|
9
|
+
|
|
10
|
+
- 🔒 **Zero Token Exposure** - Authentication tokens never reach the client-side
|
|
11
|
+
- 🚀 **Next.js Optimized** - Deep integration with App Router, Server Components, and Middleware
|
|
12
|
+
- 🛡️ **Session-Based** - Robust session management without complex token handling
|
|
13
|
+
- ⚡ **Edge Ready** - Lightweight middleware for edge deployment
|
|
14
|
+
- 🎯 **TypeScript First** - Full TypeScript support with comprehensive type definitions
|
|
15
|
+
- 🧩 **Framework Agnostic** - Works with any React-based Next.js application
|
|
16
|
+
|
|
17
|
+
## 📦 Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @ezz-auth/next
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 🚀 Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Environment Setup
|
|
26
|
+
|
|
27
|
+
Add to your `.env.local`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Core Auth System URL
|
|
31
|
+
AUTH_BASE_URL=https://auth.your-domain.com
|
|
32
|
+
|
|
33
|
+
# Your Application Credentials
|
|
34
|
+
AUTH_APP_KEY=your_public_key
|
|
35
|
+
AUTH_APP_SECRET=your_secret_key
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Initialize Routes
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx ezz-auth init
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This creates the necessary API routes for authentication callbacks.
|
|
45
|
+
|
|
46
|
+
### 3. Protect Your App
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// app/layout.tsx
|
|
50
|
+
import { AuthProvider } from "@ezz-auth/next/client";
|
|
51
|
+
import { auth } from "@ezz-auth/next/server";
|
|
52
|
+
|
|
53
|
+
export default async function RootLayout({ children }) {
|
|
54
|
+
const { user } = await auth();
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<html>
|
|
58
|
+
<body>
|
|
59
|
+
<AuthProvider initialUser={user}>
|
|
60
|
+
{children}
|
|
61
|
+
</AuthProvider>
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// app/dashboard/page.tsx
|
|
70
|
+
import { auth } from "@ezz-auth/next/server";
|
|
71
|
+
import { redirect } from "next/navigation";
|
|
72
|
+
|
|
73
|
+
export default async function Dashboard() {
|
|
74
|
+
const { user } = await auth();
|
|
75
|
+
|
|
76
|
+
if (!user) {
|
|
77
|
+
redirect("/");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return <h1>Welcome, {user.name}!</h1>;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 📚 API Reference
|
|
85
|
+
|
|
86
|
+
### Client-Side API
|
|
87
|
+
|
|
88
|
+
#### `AuthProvider`
|
|
89
|
+
|
|
90
|
+
Context provider that manages authentication state across your React application.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { AuthProvider } from "@ezz-auth/next/client";
|
|
94
|
+
|
|
95
|
+
<AuthProvider initialUser={user}>
|
|
96
|
+
<App />
|
|
97
|
+
</AuthProvider>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Props:**
|
|
101
|
+
- `initialUser` (optional): Initial user object from server-side auth
|
|
102
|
+
- `children`: React components to render
|
|
103
|
+
|
|
104
|
+
#### `useUser()`
|
|
105
|
+
|
|
106
|
+
React hook to access the current authenticated user in client components.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { useUser } from "@ezz-auth/next/client";
|
|
110
|
+
|
|
111
|
+
function Profile() {
|
|
112
|
+
const { user, isSignedIn, isLoaded } = useUser();
|
|
113
|
+
|
|
114
|
+
if (!isLoaded) return <div>Loading...</div>;
|
|
115
|
+
if (!isSignedIn) return <div>Please sign in</div>;
|
|
116
|
+
|
|
117
|
+
return <div>Hello, {user.name}!</div>;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Returns:**
|
|
122
|
+
- `user`: Current user object or `null`
|
|
123
|
+
- `isSignedIn`: Boolean indicating authentication status
|
|
124
|
+
- `isLoaded`: Boolean indicating if auth state has been determined
|
|
125
|
+
|
|
126
|
+
#### `SignInButton`
|
|
127
|
+
|
|
128
|
+
Pre-built component that redirects users to the authentication flow.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { SignInButton } from "@ezz-auth/next/client";
|
|
132
|
+
|
|
133
|
+
<SignInButton />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Props:** None (customizable via CSS classes)
|
|
137
|
+
|
|
138
|
+
#### `UserButton`
|
|
139
|
+
|
|
140
|
+
Component that displays user information and provides logout functionality.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { UserButton } from "@ezz-auth/next/client";
|
|
144
|
+
|
|
145
|
+
<UserButton />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Props:** None (customizable via CSS classes)
|
|
149
|
+
|
|
150
|
+
#### `signOut()`
|
|
151
|
+
|
|
152
|
+
Function to sign out the current user and clear their session.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { signOut } from "@ezz-auth/next/client";
|
|
156
|
+
|
|
157
|
+
function handleLogout() {
|
|
158
|
+
signOut();
|
|
159
|
+
// User will be redirected and session cleared
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Server-Side API
|
|
164
|
+
|
|
165
|
+
#### `auth()`
|
|
166
|
+
|
|
167
|
+
Primary server-side function to authenticate requests and get user data.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { auth } from "@ezz-auth/next/server";
|
|
171
|
+
|
|
172
|
+
export default async function ProtectedPage() {
|
|
173
|
+
const { user, session } = await auth();
|
|
174
|
+
|
|
175
|
+
if (!user) {
|
|
176
|
+
redirect("/login");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return <div>User: {user.email}</div>;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Returns:**
|
|
184
|
+
- `user`: User object or `null`
|
|
185
|
+
- `session`: Session object or `null`
|
|
186
|
+
|
|
187
|
+
#### `currentUser()`
|
|
188
|
+
|
|
189
|
+
Simplified function that returns only the current user (alias for `auth().user`).
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { currentUser } from "@ezz-auth/next/server";
|
|
193
|
+
|
|
194
|
+
const user = await currentUser();
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Returns:** User object or `null`
|
|
198
|
+
|
|
199
|
+
#### `requireAuth()`
|
|
200
|
+
|
|
201
|
+
Throws an error if user is not authenticated. Useful for protecting API routes.
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import { requireAuth } from "@ezz-auth/next/server";
|
|
205
|
+
|
|
206
|
+
export async function GET() {
|
|
207
|
+
const { user } = await requireAuth();
|
|
208
|
+
|
|
209
|
+
// User is guaranteed to be authenticated here
|
|
210
|
+
return Response.json({ data: "protected content" });
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Options:**
|
|
215
|
+
- `state` (optional): Custom state to pass through auth flow
|
|
216
|
+
|
|
217
|
+
**Returns:** Object with `user` and `session` (throws if unauthenticated)
|
|
218
|
+
|
|
219
|
+
#### `handleAuthCallback()`
|
|
220
|
+
|
|
221
|
+
Route handler for processing OAuth callbacks after authentication.
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
// app/api/auth/callback/route.ts
|
|
225
|
+
import { handleAuthCallback } from "@ezz-auth/next/server";
|
|
226
|
+
|
|
227
|
+
export const GET = handleAuthCallback({
|
|
228
|
+
redirectTo: "/dashboard"
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Options:**
|
|
233
|
+
- `redirectTo` (optional): Where to redirect after successful authentication (default: "/")
|
|
234
|
+
|
|
235
|
+
#### `handleLogout()`
|
|
236
|
+
|
|
237
|
+
Route handler for processing logout requests.
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
// app/api/auth/logout/route.ts
|
|
241
|
+
import { handleLogout } from "@ezz-auth/next/server";
|
|
242
|
+
|
|
243
|
+
export const POST = handleLogout();
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Middleware API
|
|
247
|
+
|
|
248
|
+
#### `authMiddleware()`
|
|
249
|
+
|
|
250
|
+
Next.js middleware for protecting routes at the edge.
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
// middleware.ts
|
|
254
|
+
import { authMiddleware } from "@ezz-auth/next/middleware";
|
|
255
|
+
|
|
256
|
+
export default authMiddleware();
|
|
257
|
+
|
|
258
|
+
export const config = {
|
|
259
|
+
matcher: ["/dashboard/:path*"]
|
|
260
|
+
};
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Returns:** Next.js middleware function
|
|
264
|
+
|
|
265
|
+
### CLI API
|
|
266
|
+
|
|
267
|
+
#### `ezz-auth init`
|
|
268
|
+
|
|
269
|
+
Scaffolds the necessary authentication routes for your Next.js application.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npx ezz-auth init
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Creates:
|
|
276
|
+
- `app/api/auth/callback/route.ts` - OAuth callback handler
|
|
277
|
+
- `app/api/auth/logout/route.ts` - Logout handler
|
|
278
|
+
|
|
279
|
+
## 🔧 Configuration
|
|
280
|
+
|
|
281
|
+
### Environment Variables
|
|
282
|
+
|
|
283
|
+
| Variable | Description | Required |
|
|
284
|
+
|----------|-------------|----------|
|
|
285
|
+
| `AUTH_BASE_URL` | URL of your Ezz Auth core system | ✅ |
|
|
286
|
+
| `AUTH_APP_KEY` | Public application key from developer console | ✅ |
|
|
287
|
+
| `AUTH_APP_SECRET` | Secret application key for server-side operations | ✅ |
|
|
288
|
+
|
|
289
|
+
### Advanced Configuration
|
|
290
|
+
|
|
291
|
+
The SDK automatically detects your Next.js app directory structure:
|
|
292
|
+
- `src/app/` (if `src` directory exists)
|
|
293
|
+
- `app/` (standard App Router location)
|
|
294
|
+
|
|
295
|
+
## 📝 Examples
|
|
296
|
+
|
|
297
|
+
### Complete App Setup
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// app/layout.tsx
|
|
301
|
+
import { AuthProvider } from "@ezz-auth/next/client";
|
|
302
|
+
import { auth } from "@ezz-auth/next/server";
|
|
303
|
+
|
|
304
|
+
export default async function Layout({ children }) {
|
|
305
|
+
const { user } = await auth();
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<html>
|
|
309
|
+
<body>
|
|
310
|
+
<AuthProvider initialUser={user}>
|
|
311
|
+
<Header />
|
|
312
|
+
{children}
|
|
313
|
+
</AuthProvider>
|
|
314
|
+
</body>
|
|
315
|
+
</html>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// app/components/Header.tsx
|
|
320
|
+
"use client";
|
|
321
|
+
import { SignInButton, UserButton, useUser } from "@ezz-auth/next/client";
|
|
322
|
+
|
|
323
|
+
export function Header() {
|
|
324
|
+
const { isSignedIn } = useUser();
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<header>
|
|
328
|
+
<nav>
|
|
329
|
+
<Link href="/">Home</Link>
|
|
330
|
+
{isSignedIn ? <UserButton /> : <SignInButton />}
|
|
331
|
+
</nav>
|
|
332
|
+
</header>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### API Route Protection
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
// app/api/user/profile/route.ts
|
|
341
|
+
import { auth } from "@ezz-auth/next/server";
|
|
342
|
+
|
|
343
|
+
export async function GET() {
|
|
344
|
+
const { user } = await auth();
|
|
345
|
+
|
|
346
|
+
if (!user) {
|
|
347
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return Response.json({ profile: user });
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Middleware Protection
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
// middleware.ts
|
|
358
|
+
import { authMiddleware } from "@ezz-auth/next/middleware";
|
|
359
|
+
|
|
360
|
+
export default authMiddleware();
|
|
361
|
+
|
|
362
|
+
export const config = {
|
|
363
|
+
matcher: ["/dashboard/:path*", "/api/protected/:path*"]
|
|
364
|
+
};
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## 🔄 How It Works
|
|
368
|
+
|
|
369
|
+
1. **Authorization Request**: User clicks sign-in, SDK redirects to Ezz Auth core with your `AUTH_APP_KEY`
|
|
370
|
+
2. **User Authentication**: Core system handles login via configured providers (OAuth, email, etc.)
|
|
371
|
+
3. **Callback Processing**: Core redirects back with authorization code
|
|
372
|
+
4. **Token Exchange**: Your app exchanges code for session token using `AUTH_APP_SECRET`
|
|
373
|
+
5. **Session Storage**: Session stored in HTTP-only, secure, SameSite=Lax cookie
|
|
374
|
+
6. **Request Validation**: Each request validates cookie against core introspection endpoint
|
|
375
|
+
|
|
376
|
+
## 🛡️ Security Features
|
|
377
|
+
|
|
378
|
+
- **No Token Exposure**: Authentication tokens never reach client-side JavaScript
|
|
379
|
+
- **HTTP-Only Cookies**: Session cookies cannot be accessed via JavaScript
|
|
380
|
+
- **Secure by Default**: Automatic security headers and cookie configuration
|
|
381
|
+
- **Server-Side Validation**: All authentication checks happen server-side
|
|
382
|
+
- **CSRF Protection**: Built-in protection against cross-site request forgery
|
|
383
|
+
|
|
384
|
+
## 🐛 Troubleshooting
|
|
385
|
+
|
|
386
|
+
### Common Issues
|
|
387
|
+
|
|
388
|
+
**"No app directory found"**
|
|
389
|
+
- Ensure you're using Next.js App Router (not Pages Router)
|
|
390
|
+
- Check that your app directory is at `app/` or `src/app/`
|
|
391
|
+
|
|
392
|
+
**"Authentication failed"**
|
|
393
|
+
- Verify `AUTH_BASE_URL` is correct and accessible
|
|
394
|
+
- Check that `AUTH_APP_KEY` and `AUTH_APP_SECRET` are valid
|
|
395
|
+
- Ensure callback URL is configured in your auth provider
|
|
396
|
+
|
|
397
|
+
**"Session not persisting"**
|
|
398
|
+
- Check that cookies are enabled in the browser
|
|
399
|
+
- Verify your domain configuration matches the auth core
|
|
400
|
+
|
|
401
|
+
## 📄 License
|
|
402
|
+
|
|
403
|
+
MIT © [Your Organization]
|
|
404
|
+
|
|
405
|
+
## 🤝 Contributing
|
|
406
|
+
|
|
407
|
+
1. Fork the repository
|
|
408
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
409
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
410
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
411
|
+
5. Open a Pull Request
|
|
412
|
+
|
|
413
|
+
## 📞 Support
|
|
414
|
+
|
|
415
|
+
- 📖 [Documentation](https://docs.ezz-auth.com)
|
|
416
|
+
- 💬 [Discord Community](https://discord.gg/ezz-auth)
|
|
417
|
+
- 🐛 [Issue Tracker](https://github.com/your-org/ezz-auth/issues)
|
|
418
|
+
- 📧 [Email Support](mailto:support@ezz-auth.com)
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import url from "url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
8
|
+
const projectRoot = process.cwd();
|
|
9
|
+
|
|
10
|
+
const command = process.argv[2];
|
|
11
|
+
|
|
12
|
+
/* ---------------- helpers ---------------- */
|
|
13
|
+
|
|
14
|
+
function findAppDir() {
|
|
15
|
+
const srcApp = path.join(projectRoot, "src", "app");
|
|
16
|
+
const app = path.join(projectRoot, "app");
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(srcApp)) return srcApp;
|
|
19
|
+
if (fs.existsSync(app)) return app;
|
|
20
|
+
|
|
21
|
+
console.error("❌ No app or src/app directory found");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function copyTemplate(relativePath) {
|
|
26
|
+
const source = path.join(__dirname, "..", "templates", relativePath);
|
|
27
|
+
const target = path.join(
|
|
28
|
+
findAppDir(),
|
|
29
|
+
relativePath.replace(/^app\//, "")
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(target)) {
|
|
35
|
+
console.log("⚠ Skipped:", target);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fs.copyFileSync(source, target);
|
|
40
|
+
console.log("✔ Created:", target);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ---------------- commands ---------------- */
|
|
44
|
+
|
|
45
|
+
function runInit() {
|
|
46
|
+
copyTemplate("app/api/auth/callback/route.ts");
|
|
47
|
+
copyTemplate("app/api/auth/logout/route.ts");
|
|
48
|
+
|
|
49
|
+
console.log("\n✅ ezz-auth initialized");
|
|
50
|
+
console.log("Next steps:");
|
|
51
|
+
console.log("1. Set env variables");
|
|
52
|
+
console.log("2. Redirect users to ezz-auth login");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ---------------- entry ---------------- */
|
|
56
|
+
|
|
57
|
+
if (command === "init") {
|
|
58
|
+
runInit();
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`
|
|
61
|
+
Usage:
|
|
62
|
+
npx ezz-auth init
|
|
63
|
+
|
|
64
|
+
Description:
|
|
65
|
+
Scaffold ezz-auth routes for Next.js App Router
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
"use client";
|
|
3
|
+
import React, { createContext, useContext } from "react";
|
|
4
|
+
const AuthContext = createContext(null);
|
|
5
|
+
|
|
6
|
+
export function AuthProvider({ children, user }) {
|
|
7
|
+
return (
|
|
8
|
+
<AuthContext.Provider value={{ user, isSignedIn: !!user }}>
|
|
9
|
+
{children}
|
|
10
|
+
</AuthContext.Provider>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useAuthContext() {
|
|
15
|
+
const ctx = useContext(AuthContext);
|
|
16
|
+
if (!ctx) throw new Error("AuthProvider missing");
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export function SignInButton({ state }) {
|
|
4
|
+
const signIn = () => {
|
|
5
|
+
const url = new URL(
|
|
6
|
+
"/api/app/v1/authorize",
|
|
7
|
+
process.env.NEXT_PUBLIC_AUTH_BASE_URL
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
url.searchParams.set(
|
|
11
|
+
"app_key",
|
|
12
|
+
process.env.NEXT_PUBLIC_AUTH_PUBLIC_KEY
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
url.searchParams.set(
|
|
16
|
+
"redirect_uri",
|
|
17
|
+
window.location.origin + "/api/auth/callback"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// ✅ ADD THIS
|
|
21
|
+
const finalState = state ?? window.location.pathname;
|
|
22
|
+
url.searchParams.set("state", finalState);
|
|
23
|
+
|
|
24
|
+
window.location.href = url.toString();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return <button onClick={signIn}>Sign in</button>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function AuthProvider(props: {
|
|
2
|
+
children: React.ReactNode;
|
|
3
|
+
user: any;
|
|
4
|
+
}): JSX.Element;
|
|
5
|
+
|
|
6
|
+
export function useUser(): {
|
|
7
|
+
user: any;
|
|
8
|
+
isSignedIn: boolean;
|
|
9
|
+
isLoaded: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function SignInButton(): JSX.Element;
|
|
13
|
+
|
|
14
|
+
export function UserButton(): JSX.Element;
|
|
15
|
+
|
|
16
|
+
export function signOut(): void;
|
package/client/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useUser } from "./useUser.js";
|
|
4
|
+
import { signOut } from "./signOut.js";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuSeparator,
|
|
13
|
+
} from "@/components/ui/dropdown-menu";
|
|
14
|
+
|
|
15
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
|
|
18
|
+
export function UserButton() {
|
|
19
|
+
const { user, isSignedIn, isLoaded } = useUser();
|
|
20
|
+
|
|
21
|
+
if (!isLoaded || !isSignedIn || !user) return null;
|
|
22
|
+
|
|
23
|
+
const initial =
|
|
24
|
+
user.name?.charAt(0)?.toUpperCase() ||
|
|
25
|
+
user.email?.charAt(0)?.toUpperCase() ||
|
|
26
|
+
"?";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<DropdownMenu>
|
|
30
|
+
<DropdownMenuTrigger asChild>
|
|
31
|
+
<Button
|
|
32
|
+
variant="ghost"
|
|
33
|
+
size="icon"
|
|
34
|
+
className="rounded-full"
|
|
35
|
+
>
|
|
36
|
+
<Avatar className="h-8 w-8">
|
|
37
|
+
<AvatarFallback>{initial}</AvatarFallback>
|
|
38
|
+
</Avatar>
|
|
39
|
+
</Button>
|
|
40
|
+
</DropdownMenuTrigger>
|
|
41
|
+
|
|
42
|
+
<DropdownMenuContent align="end" className="w-64">
|
|
43
|
+
<DropdownMenuLabel className="font-normal">
|
|
44
|
+
<div className="flex flex-col space-y-1">
|
|
45
|
+
<p className="text-xs text-muted-foreground">
|
|
46
|
+
Signed in as
|
|
47
|
+
</p>
|
|
48
|
+
<p className="text-sm font-medium leading-none">
|
|
49
|
+
{user.email}
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
</DropdownMenuLabel>
|
|
53
|
+
|
|
54
|
+
<DropdownMenuSeparator />
|
|
55
|
+
|
|
56
|
+
<DropdownMenuItem
|
|
57
|
+
onClick={signOut}
|
|
58
|
+
className="cursor-pointer"
|
|
59
|
+
>
|
|
60
|
+
Log out
|
|
61
|
+
</DropdownMenuItem>
|
|
62
|
+
</DropdownMenuContent>
|
|
63
|
+
</DropdownMenu>
|
|
64
|
+
);
|
|
65
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
export function authMiddleware() {
|
|
4
|
+
return function middleware(req) {
|
|
5
|
+
const session = req.cookies.get("__auth_session")?.value;
|
|
6
|
+
if (!session) {
|
|
7
|
+
const loginUrl = new URL("/api/app/v1/authorize", process.env.NEXT_PUBLIC_AUTH_BASE_URL);
|
|
8
|
+
loginUrl.searchParams.set("app_key", process.env.NEXT_PUBLIC_AUTH_PUBLIC_KEY);
|
|
9
|
+
loginUrl.searchParams.set("redirect_uri", req.nextUrl.href);
|
|
10
|
+
return NextResponse.redirect(loginUrl);
|
|
11
|
+
}
|
|
12
|
+
return NextResponse.next();
|
|
13
|
+
};
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@auth-ezz/nextjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"types": "./index.d.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ezz-auth": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"default": "./client/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./client": {
|
|
19
|
+
"types": "./client/index.d.ts",
|
|
20
|
+
"default": "./client/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./server": {
|
|
23
|
+
"types": "./server/index.d.ts",
|
|
24
|
+
"default": "./server/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./middleware": {
|
|
27
|
+
"types": "./middleware/authMiddleware.d.ts",
|
|
28
|
+
"default": "./middleware/authMiddleware.js"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/server/auth.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import { cookies } from "next/headers";
|
|
3
|
+
const AUTH_COOKIE = "user_session";
|
|
4
|
+
|
|
5
|
+
export async function auth() {
|
|
6
|
+
const cookieStore = await cookies();
|
|
7
|
+
const sessionToken = cookieStore.get(AUTH_COOKIE)?.value;
|
|
8
|
+
if (!sessionToken) return { user: null, session: null };
|
|
9
|
+
|
|
10
|
+
const res = await fetch(
|
|
11
|
+
`${process.env.AUTH_BASE_URL}/api/app/v1/userinfo`,
|
|
12
|
+
{ headers: { Authorization: `Bearer ${sessionToken}` }, cache: "no-store" }
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
if (!res.ok) return { user: null, session: null };
|
|
16
|
+
const data = await res.json();
|
|
17
|
+
return { user: data.user, session: { token: sessionToken } };
|
|
18
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
export function handleAuthCallback(options = {}) {
|
|
4
|
+
const fallbackRedirect = options.redirectTo ?? "/";
|
|
5
|
+
|
|
6
|
+
return async function GET(req) {
|
|
7
|
+
const { searchParams, origin } = req.nextUrl;
|
|
8
|
+
|
|
9
|
+
const code = searchParams.get("code");
|
|
10
|
+
const rawState = searchParams.get("state");
|
|
11
|
+
|
|
12
|
+
// ✅ SAFETY CHECK
|
|
13
|
+
const state =
|
|
14
|
+
rawState && rawState.startsWith("/") ? rawState : fallbackRedirect;
|
|
15
|
+
|
|
16
|
+
if (!code) {
|
|
17
|
+
return NextResponse.redirect(new URL(state, origin));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const res = await fetch(
|
|
21
|
+
`${process.env.AUTH_BASE_URL}/api/app/v1/token`,
|
|
22
|
+
{
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
Authorization: `Bearer ${process.env.AUTH_APP_SECRET}`,
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({ code }),
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
return NextResponse.redirect(new URL(state, origin));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
|
|
38
|
+
const response = NextResponse.redirect(new URL(state, origin));
|
|
39
|
+
|
|
40
|
+
response.cookies.set("user_session", data.sessionToken, {
|
|
41
|
+
httpOnly: true,
|
|
42
|
+
sameSite: "lax",
|
|
43
|
+
secure: process.env.NODE_ENV === "production",
|
|
44
|
+
path: "/",
|
|
45
|
+
expires: new Date(data.expiresAt),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return response;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function auth(): Promise<{
|
|
2
|
+
user: any | null;
|
|
3
|
+
session: any | null;
|
|
4
|
+
}>;
|
|
5
|
+
|
|
6
|
+
export function currentUser(): Promise<any | null>;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function handleAuthCallback(options?: {
|
|
11
|
+
redirectTo?: string;
|
|
12
|
+
}): (req: import("next/server").NextRequest) =>
|
|
13
|
+
Promise<import("next/server").NextResponse>;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export function handleLogout(): (req: import("next/server").NextRequest) =>
|
|
17
|
+
Promise<import("next/server").NextResponse>;
|
|
18
|
+
|
|
19
|
+
export function requireAuth(options?: {
|
|
20
|
+
state?: string;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
user: any | null;
|
|
23
|
+
session: any | null;
|
|
24
|
+
}>;
|
package/server/index.js
ADDED
package/server/logout.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
export async function logout(req) {
|
|
4
|
+
const sessionToken = req.cookies.get("user_session")?.value;
|
|
5
|
+
|
|
6
|
+
const res = NextResponse.json({ ok: true });
|
|
7
|
+
res.cookies.delete("user_session", { path: "/" });
|
|
8
|
+
|
|
9
|
+
if (!sessionToken) return res;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await fetch(`${process.env.AUTH_BASE_URL}/api/app/v1/logout`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${process.env.AUTH_APP_SECRET}`,
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({ sessionToken }),
|
|
19
|
+
});
|
|
20
|
+
} catch {
|
|
21
|
+
// logout must never block UX
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
import { auth } from "./auth.js";
|
|
3
|
+
|
|
4
|
+
export async function requireAuth(options = {}) {
|
|
5
|
+
const result = await auth();
|
|
6
|
+
|
|
7
|
+
if (result.user) {
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const state = options.state || "/";
|
|
12
|
+
|
|
13
|
+
// 🔒 Safety check
|
|
14
|
+
const safeState = state.startsWith("/") ? state : "/";
|
|
15
|
+
|
|
16
|
+
const url = new URL(
|
|
17
|
+
"/api/app/v1/authorize",
|
|
18
|
+
process.env.NEXT_PUBLIC_AUTH_BASE_URL
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
url.searchParams.set(
|
|
22
|
+
"app_key",
|
|
23
|
+
process.env.NEXT_PUBLIC_AUTH_PUBLIC_KEY
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
url.searchParams.set(
|
|
27
|
+
"redirect_uri",
|
|
28
|
+
`${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
url.searchParams.set("state", safeState);
|
|
32
|
+
|
|
33
|
+
redirect(url.toString());
|
|
34
|
+
}
|