@smittdev/next-jwt-auth 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/dist/files/lib/auth/client/index.ts +2 -0
- package/dist/files/lib/auth/client/provider.tsx +424 -0
- package/dist/files/lib/auth/config.ts +54 -0
- package/dist/files/lib/auth/core/config.ts +57 -0
- package/dist/files/lib/auth/core/cookies.ts +92 -0
- package/dist/files/lib/auth/core/index.ts +14 -0
- package/dist/files/lib/auth/core/jwt.ts +65 -0
- package/dist/files/lib/auth/index.ts +112 -0
- package/dist/files/lib/auth/middleware/auth-middleware.ts +191 -0
- package/dist/files/lib/auth/middleware/index.ts +3 -0
- package/dist/files/lib/auth/server/actions.ts +352 -0
- package/dist/files/lib/auth/server/fetchers.ts +40 -0
- package/dist/files/lib/auth/server/index.ts +12 -0
- package/dist/files/lib/auth/server/session.ts +158 -0
- package/dist/files/lib/auth/types.ts +227 -0
- package/dist/index.js +1128 -0
- package/package.json +41 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import {
|
|
5
|
+
clearTokenCookies,
|
|
6
|
+
getTokensFromCookies,
|
|
7
|
+
isTokenValid,
|
|
8
|
+
setTokenCookies,
|
|
9
|
+
} from "../core";
|
|
10
|
+
import type {
|
|
11
|
+
ActionResult,
|
|
12
|
+
LoginActionOptions,
|
|
13
|
+
SessionActionData,
|
|
14
|
+
} from "../types";
|
|
15
|
+
import { TokenPairSchema } from "../types";
|
|
16
|
+
import { getGlobalAuthConfig, debugLog } from "../config";
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function extractErrorMessage(error: unknown, fallback: string): string {
|
|
21
|
+
if (error instanceof Error) return error.message;
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateTokenPair(tokens: unknown) {
|
|
26
|
+
return TokenPairSchema.parse(tokens);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isNextRedirectError(error: unknown): boolean {
|
|
30
|
+
return (
|
|
31
|
+
typeof error === "object" &&
|
|
32
|
+
error !== null &&
|
|
33
|
+
"digest" in error &&
|
|
34
|
+
typeof (error as Record<string, unknown>).digest === "string" &&
|
|
35
|
+
((error as Record<string, unknown>).digest as string).startsWith(
|
|
36
|
+
"NEXT_REDIRECT",
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates a callbackUrl to prevent open-redirect attacks.
|
|
43
|
+
* Only root-relative paths (starting with "/" but not "//") are allowed.
|
|
44
|
+
* Any other value — absolute URLs, protocol-relative URLs, empty strings —
|
|
45
|
+
* is rejected and returns undefined, falling back to pages.home.
|
|
46
|
+
*/
|
|
47
|
+
function sanitizeCallbackUrl(url: string | undefined): string | undefined {
|
|
48
|
+
if (!url) return undefined;
|
|
49
|
+
// Allow root-relative paths only — blocks `//evil.com` and `https://evil.com`
|
|
50
|
+
if (url.startsWith("/") && !url.startsWith("//")) return url;
|
|
51
|
+
debugLog("sanitizeCallbackUrl: rejected unsafe callbackUrl", { url });
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Actions ─────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fetches the current session from cookies and returns fresh session data.
|
|
59
|
+
*
|
|
60
|
+
* If the access token is expired but a valid refresh token exists, this action
|
|
61
|
+
* silently rotates the token pair (calls adapter.refreshToken, updates cookies)
|
|
62
|
+
* before resolving the session — the caller always receives a valid, up-to-date
|
|
63
|
+
* session or null, never a stale/expired one.
|
|
64
|
+
*
|
|
65
|
+
* Returns { success: true, data: null } if no valid session exists at all.
|
|
66
|
+
*
|
|
67
|
+
* Used internally by AuthProvider on mount and on tab focus via fetchSession.
|
|
68
|
+
*/
|
|
69
|
+
export async function fetchSessionAction(): Promise<
|
|
70
|
+
ActionResult<SessionActionData | null>
|
|
71
|
+
> {
|
|
72
|
+
debugLog("fetchSessionAction: called");
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const config = getGlobalAuthConfig();
|
|
76
|
+
const tokens = await getTokensFromCookies(config);
|
|
77
|
+
|
|
78
|
+
if (!tokens) {
|
|
79
|
+
debugLog("fetchSessionAction: no tokens found in cookies");
|
|
80
|
+
return { success: true, data: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let { accessToken, refreshToken } = tokens;
|
|
84
|
+
|
|
85
|
+
if (!isTokenValid(accessToken)) {
|
|
86
|
+
debugLog("fetchSessionAction: access token invalid — attempting refresh");
|
|
87
|
+
|
|
88
|
+
if (!isTokenValid(refreshToken)) {
|
|
89
|
+
debugLog(
|
|
90
|
+
"fetchSessionAction: refresh token also invalid — clearing cookies",
|
|
91
|
+
);
|
|
92
|
+
await clearTokenCookies(config);
|
|
93
|
+
return { success: true, data: null };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const refreshed = await config.adapter.refreshToken(refreshToken);
|
|
98
|
+
const validated = validateTokenPair(refreshed);
|
|
99
|
+
await setTokenCookies(validated, config);
|
|
100
|
+
accessToken = validated.accessToken;
|
|
101
|
+
refreshToken = validated.refreshToken;
|
|
102
|
+
debugLog("fetchSessionAction: token refresh successful");
|
|
103
|
+
} catch (refreshError) {
|
|
104
|
+
debugLog(
|
|
105
|
+
"fetchSessionAction: token refresh failed — clearing cookies",
|
|
106
|
+
{
|
|
107
|
+
error:
|
|
108
|
+
refreshError instanceof Error
|
|
109
|
+
? refreshError.message
|
|
110
|
+
: String(refreshError),
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
await clearTokenCookies(config);
|
|
114
|
+
return { success: true, data: null };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const user = await config.adapter.fetchUser(accessToken);
|
|
119
|
+
debugLog("fetchSessionAction: session resolved", { userId: user.id });
|
|
120
|
+
return { success: true, data: { accessToken, refreshToken, user } };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
debugLog("fetchSessionAction: unexpected error", {
|
|
123
|
+
error: error instanceof Error ? error.message : String(error),
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: extractErrorMessage(error, "Failed to fetch session."),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Logs the user in with the provided credentials.
|
|
134
|
+
*
|
|
135
|
+
* By default (`redirect: true`) redirects after a successful login using
|
|
136
|
+
* the first truthy value in this priority order:
|
|
137
|
+
* 1. `options.redirectTo` — explicit override, always wins
|
|
138
|
+
* 2. `options.callbackUrl` — typically the `?callbackUrl=` param set by
|
|
139
|
+
* requireSession(); validated to prevent open-redirect attacks
|
|
140
|
+
* 3. `config.pages.home` — the configured default
|
|
141
|
+
*
|
|
142
|
+
* Set `redirect: false` to disable the automatic redirect and handle
|
|
143
|
+
* navigation yourself on the client based on the returned ActionResult.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* // Default — redirect to pages.home
|
|
147
|
+
* await loginAction({ email, password });
|
|
148
|
+
*
|
|
149
|
+
* // Honour the callbackUrl from the URL search params
|
|
150
|
+
* const result = await loginAction(
|
|
151
|
+
* { email, password },
|
|
152
|
+
* { callbackUrl: searchParams.get("callbackUrl") ?? undefined },
|
|
153
|
+
* );
|
|
154
|
+
*
|
|
155
|
+
* // Disable redirect entirely — handle navigation on the client
|
|
156
|
+
* const result = await loginAction(
|
|
157
|
+
* { email, password },
|
|
158
|
+
* { redirect: false },
|
|
159
|
+
* );
|
|
160
|
+
* if (result.success) router.push("/dashboard");
|
|
161
|
+
* else setError(result.error);
|
|
162
|
+
*/
|
|
163
|
+
export async function loginAction(
|
|
164
|
+
credentials: Record<string, unknown>,
|
|
165
|
+
options: LoginActionOptions = {},
|
|
166
|
+
): Promise<ActionResult<SessionActionData>> {
|
|
167
|
+
const { redirect: shouldRedirect = true, redirectTo, callbackUrl } = options;
|
|
168
|
+
|
|
169
|
+
debugLog("loginAction: called", {
|
|
170
|
+
shouldRedirect,
|
|
171
|
+
hasCallbackUrl: !!callbackUrl,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const config = getGlobalAuthConfig();
|
|
176
|
+
const rawTokens = await config.adapter.login(credentials);
|
|
177
|
+
const tokens = validateTokenPair(rawTokens);
|
|
178
|
+
await setTokenCookies(tokens, config);
|
|
179
|
+
const user = await config.adapter.fetchUser(tokens.accessToken);
|
|
180
|
+
|
|
181
|
+
debugLog("loginAction: login successful", { userId: user.id });
|
|
182
|
+
|
|
183
|
+
const result: ActionResult<SessionActionData> = {
|
|
184
|
+
success: true,
|
|
185
|
+
data: {
|
|
186
|
+
accessToken: tokens.accessToken,
|
|
187
|
+
refreshToken: tokens.refreshToken,
|
|
188
|
+
user,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if (shouldRedirect) {
|
|
193
|
+
// Priority: explicit redirectTo > sanitized callbackUrl > configured default
|
|
194
|
+
const destination =
|
|
195
|
+
redirectTo ??
|
|
196
|
+
sanitizeCallbackUrl(callbackUrl) ??
|
|
197
|
+
config.pages.home;
|
|
198
|
+
|
|
199
|
+
debugLog("loginAction: redirecting", { destination });
|
|
200
|
+
// redirect() throws internally — this line never returns
|
|
201
|
+
redirect(destination);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (isNextRedirectError(error)) throw error;
|
|
207
|
+
|
|
208
|
+
debugLog("loginAction: login failed", {
|
|
209
|
+
error: error instanceof Error ? error.message : String(error),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const config = getGlobalAuthConfig();
|
|
213
|
+
await clearTokenCookies(config).catch(() => {});
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error: extractErrorMessage(error, "Login failed. Please try again."),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Logs the user out.
|
|
223
|
+
*
|
|
224
|
+
* By default (`redirect: true`) clears cookies and redirects to `redirectTo`
|
|
225
|
+
* or `pages.signIn` — matching next-auth behaviour.
|
|
226
|
+
*
|
|
227
|
+
* Set `redirect: false` to disable the automatic redirect and handle
|
|
228
|
+
* navigation yourself on the client based on the returned ActionResult.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* // Default — redirect happens automatically
|
|
232
|
+
* await logoutAction();
|
|
233
|
+
*
|
|
234
|
+
* // Custom redirect target
|
|
235
|
+
* await logoutAction({ redirectTo: "/login" });
|
|
236
|
+
*
|
|
237
|
+
* // Disable redirect — handle it on the client
|
|
238
|
+
* const result = await logoutAction({ redirect: false });
|
|
239
|
+
* if (result.success) router.replace("/");
|
|
240
|
+
*/
|
|
241
|
+
export async function logoutAction(
|
|
242
|
+
options: { redirect?: boolean; redirectTo?: string } = {},
|
|
243
|
+
): Promise<ActionResult<null>> {
|
|
244
|
+
const { redirect: shouldRedirect = true, redirectTo } = options;
|
|
245
|
+
|
|
246
|
+
debugLog("logoutAction: called", { shouldRedirect });
|
|
247
|
+
|
|
248
|
+
const config = getGlobalAuthConfig();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const tokens = await getTokensFromCookies(config);
|
|
252
|
+
|
|
253
|
+
if (tokens && config.adapter.logout) {
|
|
254
|
+
try {
|
|
255
|
+
await config.adapter.logout(tokens);
|
|
256
|
+
debugLog("logoutAction: adapter.logout() completed");
|
|
257
|
+
} catch (adapterError) {
|
|
258
|
+
// Non-fatal — cookies will still be cleared regardless
|
|
259
|
+
debugLog(
|
|
260
|
+
"logoutAction: adapter.logout() threw — cookies will still be cleared",
|
|
261
|
+
{
|
|
262
|
+
error:
|
|
263
|
+
adapterError instanceof Error
|
|
264
|
+
? adapterError.message
|
|
265
|
+
: String(adapterError),
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
console.error(
|
|
269
|
+
"[next-jwt-auth] logoutAction: adapter.logout() threw. Cookies will still be cleared.",
|
|
270
|
+
adapterError,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await clearTokenCookies(config);
|
|
276
|
+
debugLog("logoutAction: cookies cleared");
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (isNextRedirectError(error)) throw error;
|
|
279
|
+
debugLog("logoutAction: unexpected error", {
|
|
280
|
+
error: error instanceof Error ? error.message : String(error),
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
error: extractErrorMessage(error, "Logout failed. Please try again."),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (shouldRedirect) {
|
|
289
|
+
const destination = redirectTo ?? config.pages.signIn;
|
|
290
|
+
debugLog("logoutAction: redirecting", { destination });
|
|
291
|
+
// redirect() is called outside try/catch so it is never swallowed
|
|
292
|
+
redirect(destination);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { success: true, data: null };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Updates the session's access token manually.
|
|
300
|
+
*
|
|
301
|
+
* This is useful when the user is handling token refreshes on the client side
|
|
302
|
+
* via an Axios interceptor (or similar) instead of relying on the Next.js middleware.
|
|
303
|
+
* If the user's external API returns a new access token, they can call this action
|
|
304
|
+
* to sync it into the HttpOnly cookies so Server Components can see it.
|
|
305
|
+
*/
|
|
306
|
+
export async function updateSessionTokenAction(
|
|
307
|
+
newAccessToken: string,
|
|
308
|
+
): Promise<ActionResult<SessionActionData>> {
|
|
309
|
+
debugLog("updateSessionTokenAction: called");
|
|
310
|
+
|
|
311
|
+
if (!newAccessToken || typeof newAccessToken !== "string") {
|
|
312
|
+
return { success: false, error: "Invalid access token provided." };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const config = getGlobalAuthConfig();
|
|
317
|
+
const tokens = await getTokensFromCookies(config);
|
|
318
|
+
|
|
319
|
+
if (!tokens) {
|
|
320
|
+
return { success: false, error: "No active session to update." };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const newTokens = {
|
|
324
|
+
accessToken: newAccessToken,
|
|
325
|
+
refreshToken: tokens.refreshToken,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const validated = validateTokenPair(newTokens);
|
|
329
|
+
await setTokenCookies(validated, config);
|
|
330
|
+
const user = await config.adapter.fetchUser(validated.accessToken);
|
|
331
|
+
|
|
332
|
+
debugLog("updateSessionTokenAction: session token updated", { userId: user.id });
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
success: true,
|
|
336
|
+
data: {
|
|
337
|
+
accessToken: validated.accessToken,
|
|
338
|
+
refreshToken: validated.refreshToken,
|
|
339
|
+
user,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
debugLog("updateSessionTokenAction: unexpected error", {
|
|
344
|
+
error: error instanceof Error ? error.message : String(error),
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: extractErrorMessage(error, "Failed to update session token."),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// lib/auth/server/fetchers.ts
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import type { Session } from "../types";
|
|
4
|
+
import { getSession } from "./session";
|
|
5
|
+
import { getGlobalAuthConfig } from "../config";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Runs a callback with the current session if one exists.
|
|
9
|
+
* Returns null (or the provided defaultValue) if the user is not authenticated.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const data = await auth.withSession((session) => fetchUserData(session.accessToken));
|
|
13
|
+
*/
|
|
14
|
+
export async function withSession<TResult>(
|
|
15
|
+
callback: (session: Session) => TResult | Promise<TResult>,
|
|
16
|
+
defaultValue?: TResult,
|
|
17
|
+
): Promise<TResult | null> {
|
|
18
|
+
const session = await getSession();
|
|
19
|
+
if (!session) {
|
|
20
|
+
return defaultValue !== undefined ? defaultValue : null;
|
|
21
|
+
}
|
|
22
|
+
return callback(session);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runs a callback with the current session, or redirects to the sign-in page.
|
|
27
|
+
* Use in Server Components or server actions where authentication is required.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const data = await auth.withRequiredSession((session) => fetchProfile(session.user.id));
|
|
31
|
+
*/
|
|
32
|
+
export async function withRequiredSession<TResult>(
|
|
33
|
+
callback: (session: Session) => TResult | Promise<TResult>,
|
|
34
|
+
): Promise<TResult> {
|
|
35
|
+
const config = getGlobalAuthConfig();
|
|
36
|
+
const session = await getSession();
|
|
37
|
+
if (!session) redirect(config.pages.signIn);
|
|
38
|
+
return callback(session);
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// lib/auth/server/index.ts
|
|
2
|
+
export {
|
|
3
|
+
getSession,
|
|
4
|
+
getAccessToken,
|
|
5
|
+
getRefreshToken,
|
|
6
|
+
getUser,
|
|
7
|
+
requireSession,
|
|
8
|
+
} from "./session";
|
|
9
|
+
|
|
10
|
+
export { fetchSessionAction, loginAction, logoutAction } from "./actions";
|
|
11
|
+
|
|
12
|
+
export { withSession, withRequiredSession } from "./fetchers";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// lib/auth/server/session.ts
|
|
2
|
+
import { cache } from "react";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { headers } from "next/headers";
|
|
5
|
+
import { getTokensFromCookies, isTokenValid } from "../core";
|
|
6
|
+
import type { Session, SessionUser } from "../types";
|
|
7
|
+
import { getGlobalAuthConfig, debugLog } from "../config";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Module-level cached resolver. React's `cache()` deduplicates this per request,
|
|
11
|
+
* so calling `getSession()` multiple times in one render tree costs exactly one
|
|
12
|
+
* cookie read and one adapter.fetchUser() call.
|
|
13
|
+
*
|
|
14
|
+
* Intentionally does NOT refresh tokens or write cookies — that is handled by
|
|
15
|
+
* the middleware before the request reaches this point. Attempting to set cookies
|
|
16
|
+
* during page rendering throws in Next.js (only allowed in Server Actions /
|
|
17
|
+
* Route Handlers).
|
|
18
|
+
*/
|
|
19
|
+
const resolveSession = cache(async (): Promise<Session | null> => {
|
|
20
|
+
const config = getGlobalAuthConfig();
|
|
21
|
+
const tokens = await getTokensFromCookies(config);
|
|
22
|
+
|
|
23
|
+
if (!tokens) {
|
|
24
|
+
debugLog("resolveSession: no tokens found in cookies");
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { accessToken, refreshToken } = tokens;
|
|
29
|
+
|
|
30
|
+
// If the access token is invalid here, the middleware either could not refresh
|
|
31
|
+
// (e.g. refresh token also expired) or is not running on this route.
|
|
32
|
+
// Either way, treat it as no session — do not attempt to set cookies.
|
|
33
|
+
if (!isTokenValid(accessToken)) {
|
|
34
|
+
debugLog(
|
|
35
|
+
"resolveSession: access token is invalid or expired — treating as no session",
|
|
36
|
+
);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const user = await config.adapter.fetchUser(accessToken);
|
|
42
|
+
debugLog("resolveSession: session resolved", { userId: user.id });
|
|
43
|
+
return { accessToken, refreshToken, user };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
debugLog(
|
|
46
|
+
"resolveSession: adapter.fetchUser() threw — treating as no session",
|
|
47
|
+
{
|
|
48
|
+
error: error instanceof Error ? error.message : String(error),
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the current session, or null if the user is not authenticated.
|
|
57
|
+
* Safe to call in any Server Component, layout, or server action.
|
|
58
|
+
* Results are deduplicated per request via React cache().
|
|
59
|
+
*/
|
|
60
|
+
export async function getSession(): Promise<Session | null> {
|
|
61
|
+
return resolveSession();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the current access token directly from cookies.
|
|
66
|
+
* Does NOT call fetchUser — use getSession() if you need the full session.
|
|
67
|
+
*/
|
|
68
|
+
export async function getAccessToken(): Promise<string | null> {
|
|
69
|
+
const config = getGlobalAuthConfig();
|
|
70
|
+
const tokens = await getTokensFromCookies(config);
|
|
71
|
+
if (!tokens || !isTokenValid(tokens.accessToken)) return null;
|
|
72
|
+
return tokens.accessToken;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns the current refresh token directly from cookies.
|
|
77
|
+
* Does NOT call fetchUser — use getSession() if you need the full session.
|
|
78
|
+
*/
|
|
79
|
+
export async function getRefreshToken(): Promise<string | null> {
|
|
80
|
+
const config = getGlobalAuthConfig();
|
|
81
|
+
const tokens = await getTokensFromCookies(config);
|
|
82
|
+
return tokens?.refreshToken ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the current user, or null if not authenticated.
|
|
87
|
+
*/
|
|
88
|
+
export async function getUser(): Promise<SessionUser | null> {
|
|
89
|
+
const session = await resolveSession();
|
|
90
|
+
return session?.user ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns the current session, or redirects to the sign-in page if not authenticated.
|
|
95
|
+
* Use this as a server-side guard in protected pages and layouts.
|
|
96
|
+
*
|
|
97
|
+
* When `includeCallbackUrl` is true (default), the current path is appended
|
|
98
|
+
* as a `?callbackUrl=` search param so your login page can redirect back
|
|
99
|
+
* after a successful login.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* // app/dashboard/page.tsx
|
|
103
|
+
* const session = await auth.requireSession();
|
|
104
|
+
* // session is guaranteed non-null here
|
|
105
|
+
*/
|
|
106
|
+
export async function requireSession(
|
|
107
|
+
options: { includeCallbackUrl?: boolean } = {},
|
|
108
|
+
): Promise<Session> {
|
|
109
|
+
const { includeCallbackUrl = true } = options;
|
|
110
|
+
const config = getGlobalAuthConfig();
|
|
111
|
+
const session = await resolveSession();
|
|
112
|
+
|
|
113
|
+
if (!session) {
|
|
114
|
+
if (includeCallbackUrl) {
|
|
115
|
+
try {
|
|
116
|
+
const headersList = await headers();
|
|
117
|
+
const currentPath =
|
|
118
|
+
headersList.get("x-pathname") ??
|
|
119
|
+
headersList.get("x-invoke-path") ??
|
|
120
|
+
"";
|
|
121
|
+
if (currentPath) {
|
|
122
|
+
debugLog(
|
|
123
|
+
"requireSession: unauthenticated — redirecting with callbackUrl",
|
|
124
|
+
{
|
|
125
|
+
signIn: config.pages.signIn,
|
|
126
|
+
callbackUrl: currentPath,
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
redirect(
|
|
130
|
+
`${config.pages.signIn}?callbackUrl=${encodeURIComponent(currentPath)}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (isRedirectError(error)) throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
debugLog("requireSession: unauthenticated — redirecting to signIn", {
|
|
139
|
+
signIn: config.pages.signIn,
|
|
140
|
+
});
|
|
141
|
+
redirect(config.pages.signIn);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return session;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Checks if an error is the special NEXT_REDIRECT internal error. */
|
|
148
|
+
function isRedirectError(error: unknown): boolean {
|
|
149
|
+
return (
|
|
150
|
+
typeof error === "object" &&
|
|
151
|
+
error !== null &&
|
|
152
|
+
"digest" in error &&
|
|
153
|
+
typeof (error as Record<string, unknown>).digest === "string" &&
|
|
154
|
+
((error as Record<string, unknown>).digest as string).startsWith(
|
|
155
|
+
"NEXT_REDIRECT",
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
}
|