@ingram-tech/nk-auth 0.13.5 → 0.14.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 +19 -3
- package/dist/jwt.d.ts +9 -0
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +9 -0
- package/dist/jwt.js.map +1 -1
- package/dist/server.d.ts +51 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +36 -27
- package/dist/server.js.map +1 -1
- package/package.json +6 -3
- package/src/client.ts +44 -0
- package/src/gating-internals.ts +47 -0
- package/src/id.ts +12 -0
- package/src/index.ts +36 -0
- package/src/jwt.ts +117 -0
- package/src/keys.ts +118 -0
- package/src/middleware.ts +150 -0
- package/src/options.ts +236 -0
- package/src/organization.ts +80 -0
- package/src/password.ts +103 -0
- package/src/paths.ts +19 -0
- package/src/pool.ts +26 -0
- package/src/reset-password.ts +128 -0
- package/src/server.ts +245 -0
package/src/paths.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Better Auth mounts in an Ingram site: **`/auth`**, not the framework
|
|
3
|
+
* default `/api/auth`. Auth is a first-class user-facing surface — sign-in,
|
|
4
|
+
* OAuth callbacks, the JWKS — not an internal machine API, so it lives at the
|
|
5
|
+
* site root alongside the other pages.
|
|
6
|
+
*
|
|
7
|
+
* Use it on both ends so server and client agree:
|
|
8
|
+
*
|
|
9
|
+
* betterAuth({ basePath: authBasePath, ... })
|
|
10
|
+
* createAuthClient({ baseURL, basePath: authBasePath, ... })
|
|
11
|
+
*
|
|
12
|
+
* and mount the catch-all at `app/auth/[...all]/route.ts`. OAuth redirect URIs
|
|
13
|
+
* then become `<site>/auth/callback/<provider>` and the JWKS `<site>/auth/jwks`.
|
|
14
|
+
*
|
|
15
|
+
* Sign-in pages share this namespace, and a page whose path matches a Better
|
|
16
|
+
* Auth endpoint shadows it (static segments beat the catch-all) — `nk doctor`
|
|
17
|
+
* flags such collisions.
|
|
18
|
+
*/
|
|
19
|
+
export const authBasePath = "/auth";
|
package/src/pool.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createPool } from "@ingram-tech/nk-db";
|
|
2
|
+
import type { Pool } from "pg";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A `pg` Pool for Better Auth's direct database connection.
|
|
6
|
+
*
|
|
7
|
+
* @deprecated Thin alias for `createPool` from `@ingram-tech/nk-db` — the one
|
|
8
|
+
* place pool construction and TLS handling live. Kept so existing call sites
|
|
9
|
+
* (`lib/db.ts` calling `createAuthPool({ connectionString, caCert })`) keep
|
|
10
|
+
* working, and so Better Auth shares the **exact same pool implementation** as
|
|
11
|
+
* app queries — the "one pool per process" rule. Prefer importing `createPool`
|
|
12
|
+
* directly in new code.
|
|
13
|
+
*
|
|
14
|
+
* TLS handling (CA-verified when `caCert` is set; plain-TLS for other managed
|
|
15
|
+
* hosts; no TLS for localhost), and the local `max: 1` cap (for the PGlite
|
|
16
|
+
* socket), all live in `createPool`.
|
|
17
|
+
*/
|
|
18
|
+
export const createAuthPool = (config: {
|
|
19
|
+
connectionString: string;
|
|
20
|
+
/** PEM CA cert; when set, the server cert + hostname are verified. */
|
|
21
|
+
caCert?: string;
|
|
22
|
+
}): Pool =>
|
|
23
|
+
createPool({
|
|
24
|
+
connectionString: config.connectionString,
|
|
25
|
+
caCert: config.caCert,
|
|
26
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless controller for a "set / reset your password" page — the token
|
|
3
|
+
* consumer at the end of Better Auth's forget-password round trip. nk-auth owns
|
|
4
|
+
* the *sender* (`makeEmailSenders.sendResetPassword`) and the endpoint contract;
|
|
5
|
+
* this closes the loop so a site never touches the `resetPassword` endpoint name
|
|
6
|
+
* or hand-rolls the invalid-token / success / validation state machine. The site
|
|
7
|
+
* brings its own shell (inputs, buttons, i18n) and wires it to what this returns.
|
|
8
|
+
*
|
|
9
|
+
* The same page serves both entry points, because both land on the identical URL
|
|
10
|
+
* with a `?token=`: the classic "I forgot my password" flow, and "set a password
|
|
11
|
+
* for my social-only account" (Better Auth's reset endpoint creates the
|
|
12
|
+
* credential when the user has none — see the package README).
|
|
13
|
+
*
|
|
14
|
+
* "use client";
|
|
15
|
+
* const { status, error, submit } = useResetPassword(authClient, { token });
|
|
16
|
+
* if (status === "invalid") return <InvalidLink />;
|
|
17
|
+
* if (status === "success") return <Done />;
|
|
18
|
+
* // <form onSubmit={() => submit(pw, confirm)}> … {error && t(error.code)} …
|
|
19
|
+
*/
|
|
20
|
+
import { useCallback, useState } from "react";
|
|
21
|
+
import {
|
|
22
|
+
type PasswordPolicy,
|
|
23
|
+
type ResetPasswordError,
|
|
24
|
+
validateNewPassword,
|
|
25
|
+
} from "./password.js";
|
|
26
|
+
|
|
27
|
+
export type ResetPasswordStatus =
|
|
28
|
+
/** No usable token in the URL — show an "invalid or expired link" message. */
|
|
29
|
+
| "invalid"
|
|
30
|
+
/** Token present; awaiting the user's new password. */
|
|
31
|
+
| "idle"
|
|
32
|
+
/** The reset request is in flight. */
|
|
33
|
+
| "submitting"
|
|
34
|
+
/** The password was set; the user can now sign in with it. */
|
|
35
|
+
| "success"
|
|
36
|
+
/** Validation failed, or the server rejected the reset — see `error`. */
|
|
37
|
+
| "error";
|
|
38
|
+
|
|
39
|
+
/** The slice of a Better Auth client this hook calls. */
|
|
40
|
+
export interface ResetPasswordClient {
|
|
41
|
+
resetPassword: (input: {
|
|
42
|
+
newPassword: string;
|
|
43
|
+
token: string;
|
|
44
|
+
}) => Promise<{ error?: { message?: string } | null }>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UseResetPasswordOptions extends PasswordPolicy {
|
|
48
|
+
/**
|
|
49
|
+
* The reset token from the URL (`?token=`), or null when it is missing — e.g.
|
|
50
|
+
* the link carried `?error=INVALID_TOKEN` instead. Null puts the controller in
|
|
51
|
+
* the `invalid` state before any submit.
|
|
52
|
+
*/
|
|
53
|
+
token: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UseResetPassword {
|
|
57
|
+
status: ResetPasswordStatus;
|
|
58
|
+
/** The current failure, or null. `code` is stable for i18n; `message` is an English fallback. */
|
|
59
|
+
error: ResetPasswordError | null;
|
|
60
|
+
/** Validate the pair against the policy, then call the reset endpoint. */
|
|
61
|
+
submit: (newPassword: string, confirmPassword: string) => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const RESET_FAILED_FALLBACK = "Could not reset your password.";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Drive a reset-password form. Returns the current `status`, the last `error`
|
|
68
|
+
* (with an i18n-friendly `code`), and a `submit(newPassword, confirmPassword)`
|
|
69
|
+
* that runs {@link validateNewPassword} against the policy before hitting the
|
|
70
|
+
* server. Pass the resolved `minLength` / `maxLength` when the site overrides
|
|
71
|
+
* Better Auth's defaults so client and server agree.
|
|
72
|
+
*/
|
|
73
|
+
export function useResetPassword(
|
|
74
|
+
client: ResetPasswordClient,
|
|
75
|
+
options: UseResetPasswordOptions,
|
|
76
|
+
): UseResetPassword {
|
|
77
|
+
const { token, minLength, maxLength } = options;
|
|
78
|
+
const [status, setStatus] = useState<ResetPasswordStatus>(
|
|
79
|
+
token ? "idle" : "invalid",
|
|
80
|
+
);
|
|
81
|
+
const [error, setError] = useState<ResetPasswordError | null>(null);
|
|
82
|
+
|
|
83
|
+
const submit = useCallback(
|
|
84
|
+
async (newPassword: string, confirmPassword: string): Promise<void> => {
|
|
85
|
+
if (!token) {
|
|
86
|
+
setStatus("invalid");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const validation = validateNewPassword(newPassword, confirmPassword, {
|
|
91
|
+
minLength,
|
|
92
|
+
maxLength,
|
|
93
|
+
});
|
|
94
|
+
if (validation) {
|
|
95
|
+
setError(validation);
|
|
96
|
+
setStatus("error");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setError(null);
|
|
101
|
+
setStatus("submitting");
|
|
102
|
+
try {
|
|
103
|
+
const { error: resetError } = await client.resetPassword({
|
|
104
|
+
newPassword,
|
|
105
|
+
token,
|
|
106
|
+
});
|
|
107
|
+
if (resetError) {
|
|
108
|
+
setError({
|
|
109
|
+
code: "reset_failed",
|
|
110
|
+
message: resetError.message ?? RESET_FAILED_FALLBACK,
|
|
111
|
+
});
|
|
112
|
+
setStatus("error");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setStatus("success");
|
|
116
|
+
} catch (err) {
|
|
117
|
+
setError({
|
|
118
|
+
code: "reset_failed",
|
|
119
|
+
message: err instanceof Error ? err.message : RESET_FAILED_FALLBACK,
|
|
120
|
+
});
|
|
121
|
+
setStatus("error");
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[token, minLength, maxLength, client],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return { status, error, submit };
|
|
128
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side session helpers for the App Router, at
|
|
3
|
+
* "@ingram-tech/nk-auth/server" so the framework-agnostic core entry never
|
|
4
|
+
* pulls in `next`. A site binds these once to its own Better Auth instance:
|
|
5
|
+
*
|
|
6
|
+
* // lib/auth/session.ts
|
|
7
|
+
* import { createAuthHelpers } from "@ingram-tech/nk-auth/server";
|
|
8
|
+
* import { auth } from "@/lib/auth";
|
|
9
|
+
*
|
|
10
|
+
* export const { getSession, getUser, requireSession, requireUser,
|
|
11
|
+
* redirectIfAuthenticated } = createAuthHelpers(auth);
|
|
12
|
+
*
|
|
13
|
+
* These are the **validated** authority: they resolve through
|
|
14
|
+
* `auth.api.getSession`, which checks the session against the database — not
|
|
15
|
+
* merely the presence of a cookie. The read is memoized per request with React
|
|
16
|
+
* `cache()`, so a render fanning out many `getUser()` calls validates once,
|
|
17
|
+
* not once per call. When a guard finds no valid session it redirects to the sign-in page,
|
|
18
|
+
* preserving the requested path as `next` (read from the header the middleware
|
|
19
|
+
* injects) and, when a session cookie is present-but-invalid, adding `stale=1`
|
|
20
|
+
* so the middleware clears the dead cookie. The optimistic, cookie-presence
|
|
21
|
+
* check belongs in middleware (see "@ingram-tech/nk-auth/middleware"); it may
|
|
22
|
+
* only ever push *unauthenticated* users off protected routes. The decision to
|
|
23
|
+
* send a signed-in user *away* from the sign-in page must run through
|
|
24
|
+
* `redirectIfAuthenticated` here, so a stale cookie resolves to "no session"
|
|
25
|
+
* and falls through to the form instead of ping-ponging forever.
|
|
26
|
+
*/
|
|
27
|
+
import type { IdHelper } from "@ingram-tech/nk-db/id";
|
|
28
|
+
import { cookies, headers } from "next/headers";
|
|
29
|
+
import { redirect } from "next/navigation";
|
|
30
|
+
import { cache } from "react";
|
|
31
|
+
import { NK_AUTH_PATH_HEADER, signInUrl } from "./gating-internals.js";
|
|
32
|
+
import { CREDENTIAL_PROVIDER_ID } from "./password.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate a `next` redirect param: returns it only if it's an internal,
|
|
36
|
+
* non-protocol-relative path, else null. Use on the login page before honoring
|
|
37
|
+
* `?next=` so it can't become an open redirect.
|
|
38
|
+
*/
|
|
39
|
+
export { safeNextParam as safeNext } from "./gating-internals.js";
|
|
40
|
+
|
|
41
|
+
/** The shape we need from a session: anything carrying a `user`. */
|
|
42
|
+
interface SessionLike {
|
|
43
|
+
user: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The slice of a Better Auth instance these helpers touch. Generic over the
|
|
48
|
+
* site's session type `S`, so `getSession`/`getUser` return the site's fully
|
|
49
|
+
* inferred user shape (additional fields, org id, …) — no `any`, no casts.
|
|
50
|
+
*/
|
|
51
|
+
interface AuthLike<S extends SessionLike> {
|
|
52
|
+
api: {
|
|
53
|
+
getSession: (input: { headers: Headers }) => Promise<S | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Better Auth's `/list-accounts`: the auth methods linked to the current
|
|
56
|
+
* session's user, each `{ providerId }` ("credential" for email/password,
|
|
57
|
+
* or a social provider like "google"). Present on every real `betterAuth()`
|
|
58
|
+
* instance; powers `getLinkedProviders` / `hasCredentialAccount` below.
|
|
59
|
+
*/
|
|
60
|
+
listUserAccounts: (input: {
|
|
61
|
+
headers: Headers;
|
|
62
|
+
}) => Promise<Array<{ providerId: string }>>;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The registry helpers for the ids a session carries. With these, every
|
|
68
|
+
* session read through the helpers presents `user.id` / `session.userId` as
|
|
69
|
+
* the user's public id and `session.activeOrganizationId` as the
|
|
70
|
+
* organization's, so app code compares session ids with `idColumn` reads in
|
|
71
|
+
* one form. Better Auth itself keeps working on raw uuids underneath.
|
|
72
|
+
*/
|
|
73
|
+
export interface SessionIds {
|
|
74
|
+
user: IdHelper;
|
|
75
|
+
organization?: IdHelper;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AuthHelpersOptions {
|
|
79
|
+
/** Where guards send unauthenticated users. Default `/login`. */
|
|
80
|
+
signInPath?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Encode session ids to their public form on every read (see
|
|
83
|
+
* {@link SessionIds}). Omit on a site that has no public id form: the
|
|
84
|
+
* helpers then return Better Auth's raw uuids.
|
|
85
|
+
*/
|
|
86
|
+
ids?: SessionIds;
|
|
87
|
+
/**
|
|
88
|
+
* Cookie-name fragment that identifies the Better Auth session cookie, used
|
|
89
|
+
* to detect a present-but-invalid (stale) cookie. Default `better-auth`,
|
|
90
|
+
* which matches `better-auth.session_token` and `__Secure-…` in production.
|
|
91
|
+
*/
|
|
92
|
+
sessionCookiePrefix?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const encodeIfString = (helper: IdHelper | undefined, value: unknown): unknown =>
|
|
96
|
+
helper && typeof value === "string" ? helper.encode(value) : value;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A session with its ids in public form: `user.id`, `session.userId` and
|
|
100
|
+
* `session.activeOrganizationId` (each only when present and a string). Pure —
|
|
101
|
+
* Better Auth's own object is left untouched, since it is what the adapter
|
|
102
|
+
* writes back. `encode` is idempotent, so an already-public id is unchanged.
|
|
103
|
+
*/
|
|
104
|
+
export function encodeSessionIds<S extends SessionLike>(
|
|
105
|
+
session: S,
|
|
106
|
+
ids: SessionIds,
|
|
107
|
+
): S {
|
|
108
|
+
const out: Record<string, unknown> = { ...(session as Record<string, unknown>) };
|
|
109
|
+
const user = session.user;
|
|
110
|
+
if (user && typeof user === "object" && "id" in user) {
|
|
111
|
+
out.user = { ...user, id: encodeIfString(ids.user, user.id) };
|
|
112
|
+
}
|
|
113
|
+
const inner = "session" in session ? session.session : undefined;
|
|
114
|
+
if (inner && typeof inner === "object") {
|
|
115
|
+
const rec = inner as Record<string, unknown>;
|
|
116
|
+
out.session = {
|
|
117
|
+
...rec,
|
|
118
|
+
...("userId" in rec
|
|
119
|
+
? { userId: encodeIfString(ids.user, rec.userId) }
|
|
120
|
+
: {}),
|
|
121
|
+
...("activeOrganizationId" in rec
|
|
122
|
+
? {
|
|
123
|
+
activeOrganizationId: encodeIfString(
|
|
124
|
+
ids.organization,
|
|
125
|
+
rec.activeOrganizationId,
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
: {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return out as S;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createAuthHelpers<S extends SessionLike>(
|
|
135
|
+
auth: AuthLike<S>,
|
|
136
|
+
options: AuthHelpersOptions = {},
|
|
137
|
+
) {
|
|
138
|
+
const signInPath = options.signInPath ?? "/login";
|
|
139
|
+
const cookiePrefix = options.sessionCookiePrefix ?? "better-auth";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The validated session ({ user, session, … }) or null. Memoized per request
|
|
143
|
+
* via React `cache()`: the session cookie is constant within a request, so
|
|
144
|
+
* every helper (and every component calling one) shares a single database
|
|
145
|
+
* validation. Outside a request scope (scripts, tests) `cache()` degrades to
|
|
146
|
+
* an uncached call.
|
|
147
|
+
*
|
|
148
|
+
* Caveat: code that mutates the session mid-request (e.g. a server action
|
|
149
|
+
* updating the user) and then re-reads it through these helpers sees the
|
|
150
|
+
* pre-mutation snapshot; re-read through `auth.api.getSession` directly if
|
|
151
|
+
* that ever matters.
|
|
152
|
+
*/
|
|
153
|
+
const getSession = cache(async (): Promise<S | null> => {
|
|
154
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
155
|
+
return session && options.ids
|
|
156
|
+
? encodeSessionIds(session, options.ids)
|
|
157
|
+
: session;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/** The authenticated user, or null. */
|
|
161
|
+
async function getUser(): Promise<S["user"] | null> {
|
|
162
|
+
const session = await getSession();
|
|
163
|
+
return session?.user ?? null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The sign-in redirect for a request with no valid session: keep the
|
|
168
|
+
* requested path as `next` (from the middleware-injected header), and flag
|
|
169
|
+
* `stale=1` when a session cookie is present-but-invalid so the middleware
|
|
170
|
+
* clears it. The two reads are request-scoped and cached by Next.
|
|
171
|
+
*/
|
|
172
|
+
async function signInTarget(): Promise<string> {
|
|
173
|
+
const [h, c] = await Promise.all([headers(), cookies()]);
|
|
174
|
+
const next = h.get(NK_AUTH_PATH_HEADER);
|
|
175
|
+
const stale = c.getAll().some((ck) => ck.name.includes(cookiePrefix));
|
|
176
|
+
return signInUrl(signInPath, { next, stale });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Require a signed-in user or redirect to sign-in. Returns the user (non-null)
|
|
181
|
+
* so callers keep their `const user = await requireUser()` shape. `redirect()`
|
|
182
|
+
* throws, so control never returns when signed out.
|
|
183
|
+
*/
|
|
184
|
+
async function requireUser(): Promise<S["user"]> {
|
|
185
|
+
const user = await getUser();
|
|
186
|
+
// redirect() is typed `never`, so `user` narrows to non-null below.
|
|
187
|
+
if (!user) redirect(await signInTarget());
|
|
188
|
+
return user;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Require a session or redirect, returning the full validated session (use
|
|
193
|
+
* when the caller needs more than the user — session id, active org, …).
|
|
194
|
+
*/
|
|
195
|
+
async function requireSession(): Promise<S> {
|
|
196
|
+
const session = await getSession();
|
|
197
|
+
if (!session) redirect(await signInTarget());
|
|
198
|
+
return session;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Redirect an already-signed-in user away (e.g. /login -> /dashboard). This
|
|
203
|
+
* is the *correct* place to gate the sign-in page: because it validates the
|
|
204
|
+
* session, a present-but-invalid cookie is treated as signed-out and the
|
|
205
|
+
* page renders instead of looping. Never do this in middleware.
|
|
206
|
+
*/
|
|
207
|
+
async function redirectIfAuthenticated(to: string): Promise<void> {
|
|
208
|
+
if (await getSession()) redirect(to);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The `providerId`s linked to the current session's user — "credential" for
|
|
213
|
+
* email/password, plus any social providers ("google", …). Empty when signed
|
|
214
|
+
* out. Reads Better Auth's own `/list-accounts`, so a site never queries the
|
|
215
|
+
* `account` table or hard-codes provider strings itself.
|
|
216
|
+
*/
|
|
217
|
+
async function getLinkedProviders(): Promise<string[]> {
|
|
218
|
+
const accounts = await auth.api.listUserAccounts({
|
|
219
|
+
headers: await headers(),
|
|
220
|
+
});
|
|
221
|
+
return accounts.map((account) => account.providerId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Whether the current user has an email/password credential. Drives the
|
|
226
|
+
* "Change password" vs "Set password" decision on a security page: a user who
|
|
227
|
+
* signed up purely through a social provider has none until they set one
|
|
228
|
+
* (typically via the reset-password / forgot-password flow — see the
|
|
229
|
+
* `useResetPassword` client hook). Returns false when signed out.
|
|
230
|
+
*/
|
|
231
|
+
async function hasCredentialAccount(): Promise<boolean> {
|
|
232
|
+
const providers = await getLinkedProviders();
|
|
233
|
+
return providers.includes(CREDENTIAL_PROVIDER_ID);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
getSession,
|
|
238
|
+
getUser,
|
|
239
|
+
requireSession,
|
|
240
|
+
requireUser,
|
|
241
|
+
redirectIfAuthenticated,
|
|
242
|
+
getLinkedProviders,
|
|
243
|
+
hasCredentialAccount,
|
|
244
|
+
};
|
|
245
|
+
}
|