diff --git a/docs/content/docs/basic-usage.mdx b/docs/content/docs/basic-usage.mdx
index 0667b88d8e..0d937fb74b 100644
--- a/docs/content/docs/basic-usage.mdx
+++ b/docs/content/docs/basic-usage.mdx
@@ -142,39 +142,23 @@ Once a user is signed in, you'll want to access their session. Better auth allow
### Client Side
-the client providers a `useSession` hook or a `session` object that you can use to access the session data. Each framework client implements the session getter in a reactive way. So if there are actions that affect the session (like signing out), it'll be reflected in the client.
+Better Auth provides a `useSession` hook to easily access session data on the client side. This hook is implemented in a reactive way for each supported framework, ensuring that any changes to the session (such as signing out) are immediately reflected in your UI.
```tsx title="user.tsx"
//make sure you're using the react client
import { createAuthClient } from "better-auth/react"
- const client = createAuthClient()
+ const { useSession } = createAuthClient() // [!code highlight]
export function User(){
- const session = client.useSession()
+ const {
+ data: session,
+ isPending, //loading state
+ error //error object
+ } = useSession()
returns (
-
- {
- session ? (
-
-
-
- ) : (
-
- )
- }
-
+ //...
)
}
```
@@ -263,6 +247,41 @@ the client providers a `useSession` hook or a `session` object that you can use
+
+ The `useSession` hook accepts an optional `initialData` parameter. This is particularly useful for server-side rendering (SSR) scenarios, such as when using Next.js or similar meta-frameworks.
+
+ By prefetching the session data on the server and passing it as `initialData`, you can avoid a flash of unauthenticated content.
+
+**Example usage with Next.js:**
+
+ ```tsx
+ // a server component
+ import { auth } from '@lib/auth';
+ import { headers } from 'next/headers';
+ export async function Page() {
+ const session = await auth.api.getSession({
+ headers: headers()
+ })
+ return { props: { initialSession: session } }
+ }
+```
+
+```tsx
+//a client component
+import { useSession, User, Sesssion } from '@lib/client';
+
+function MyComponent({ initialSession }: {
+initialSession: {
+ session: Session,
+ user: User
+}}) {
+const { data: session } = useSession(initialSession)
+// ...
+}
+```
+
+With this approach you'll get the initial load from the server-side but you get to keep the client-side reactivity.
+
### Server Side
The server provides a `session` object that you can use to access the session data.
diff --git a/docs/content/docs/installation.mdx b/docs/content/docs/installation.mdx
index 9f1157cfba..e6d1cf52ae 100644
--- a/docs/content/docs/installation.mdx
+++ b/docs/content/docs/installation.mdx
@@ -10,7 +10,7 @@ description: A friendly guide to installing and setting up Better Auth
Let's start by adding Better Auth to your project:
-```package-install
+```package-install
better-auth
```
diff --git a/packages/better-auth/src/adapters/internal-adapter.ts b/packages/better-auth/src/adapters/internal-adapter.ts
index 4a86c1e8c4..1a45a23ce8 100644
--- a/packages/better-auth/src/adapters/internal-adapter.ts
+++ b/packages/better-auth/src/adapters/internal-adapter.ts
@@ -197,6 +197,31 @@ export const createInternalAdapter = (
});
return account;
},
+ findAccounts: async (userId: string) => {
+ const accounts = await adapter.findMany({
+ model: tables.account.tableName,
+ where: [
+ {
+ field: "userId",
+ value: userId,
+ },
+ ],
+ });
+ return accounts;
+ },
+ updateAccount: async (accountId: string, data: Partial) => {
+ const account = await adapter.update({
+ model: tables.account.tableName,
+ where: [
+ {
+ field: "id",
+ value: accountId,
+ },
+ ],
+ update: data,
+ });
+ return account;
+ },
};
};
diff --git a/packages/better-auth/src/api/index.ts b/packages/better-auth/src/api/index.ts
index 0cbbca48f5..5d7c99a0ca 100644
--- a/packages/better-auth/src/api/index.ts
+++ b/packages/better-auth/src/api/index.ts
@@ -24,6 +24,7 @@ import { ok } from "./routes/ok";
import { signUpEmail } from "./routes/sign-up";
import { error } from "./routes/error";
import { logger } from "../utils/logger";
+import { changePassword, updateUser } from "./routes/update-user";
export function getEndpoints<
C extends AuthContext,
@@ -96,6 +97,8 @@ export function getEndpoints<
resetPassword,
verifyEmail,
sendVerificationEmail,
+ changePassword,
+ updateUser,
};
const endpoints = {
...baseEndpoints,
@@ -189,7 +192,6 @@ export const router = (
...middlewares,
],
onError(e) {
- console.log(e);
if (e instanceof APIError) {
if (e.status === "INTERNAL_SERVER_ERROR") {
logger.error(e);
diff --git a/packages/better-auth/src/api/routes/update-user.test.ts b/packages/better-auth/src/api/routes/update-user.test.ts
new file mode 100644
index 0000000000..178cc7a33e
--- /dev/null
+++ b/packages/better-auth/src/api/routes/update-user.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from "vitest";
+import { getTestInstance } from "../../test-utils/test-instance";
+
+describe("updateUser", async () => {
+ const { auth, client, testUser, sessionSetter } = await getTestInstance();
+ const headers = new Headers();
+ const session = await client.signIn.email({
+ email: testUser.email,
+ password: testUser.password,
+ options: {
+ onSuccess: sessionSetter(headers),
+ },
+ });
+ if (!session) {
+ throw new Error("No session");
+ }
+
+ it("should update the user's name", async () => {
+ const updated = await client.user.update({
+ name: "newName",
+ options: {
+ headers,
+ },
+ });
+ expect(updated.data?.name).toBe("newName");
+ });
+
+ it("should update the user's password", async () => {
+ const updated = await client.user.changePassword({
+ newPassword: "newPassword",
+ oldPassword: testUser.password,
+ options: {
+ headers,
+ },
+ });
+ expect(updated).toBeDefined();
+ const signInRes = await client.signIn.email({
+ email: testUser.email,
+ password: "newPassword",
+ });
+ expect(signInRes.data?.user).toBeDefined();
+ const signInOldPassword = await client.signIn.email({
+ email: testUser.email,
+ password: testUser.password,
+ });
+ expect(signInOldPassword.data).toBeNull();
+ });
+});
diff --git a/packages/better-auth/src/api/routes/update-user.ts b/packages/better-auth/src/api/routes/update-user.ts
new file mode 100644
index 0000000000..da7e01e4cc
--- /dev/null
+++ b/packages/better-auth/src/api/routes/update-user.ts
@@ -0,0 +1,95 @@
+import { z } from "zod";
+import { createAuthEndpoint } from "../call";
+import { sessionMiddleware } from "../middlewares/session";
+import { alphabet, generateRandomString } from "oslo/crypto";
+
+export const updateUser = createAuthEndpoint(
+ "/user/update",
+ {
+ method: "POST",
+ body: z.object({
+ name: z.string().optional(),
+ image: z.string().optional(),
+ }),
+ use: [sessionMiddleware],
+ },
+ async (ctx) => {
+ const { name, image } = ctx.body;
+ const session = ctx.context.session;
+ const user = await ctx.context.internalAdapter.updateUserByEmail(
+ session.user.email,
+ {
+ name,
+ image,
+ },
+ );
+ return ctx.json(user);
+ },
+);
+
+export const changePassword = createAuthEndpoint(
+ "/user/change-password",
+ {
+ method: "POST",
+ body: z.object({
+ newPassword: z.string(),
+ /**
+ * If the user has not set a password yet,
+ * they can set it with this field.
+ */
+ oldPassword: z.string(),
+ }),
+ use: [sessionMiddleware],
+ },
+ async (ctx) => {
+ const { newPassword, oldPassword } = ctx.body;
+ const session = ctx.context.session;
+ const minPasswordLength =
+ ctx.context.options?.emailAndPassword?.minPasswordLength || 8;
+ if (newPassword.length < minPasswordLength) {
+ ctx.context.logger.error("Password is too short");
+ return ctx.json(null, {
+ status: 400,
+ body: { message: "Password is too short" },
+ });
+ }
+ const accounts = await ctx.context.internalAdapter.findAccounts(
+ session.user.id,
+ );
+ const account = accounts.find(
+ (account) => account.providerId === "credential" && account.password,
+ );
+ const passwordHash = await ctx.context.password.hash(newPassword);
+ if (!account) {
+ await ctx.context.internalAdapter.linkAccount({
+ id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
+ userId: session.user.id,
+ providerId: "credential",
+ accountId: session.user.id,
+ password: passwordHash,
+ });
+ return ctx.json(session.user);
+ }
+ if (account.password) {
+ const verify = await ctx.context.password.verify(
+ account.password,
+ oldPassword,
+ );
+ if (!verify) {
+ return ctx.json(null, {
+ status: 400,
+ body: { message: "Invalid password" },
+ });
+ }
+ }
+ await ctx.context.internalAdapter.updateAccount(account.id, {
+ password: passwordHash,
+ });
+ // TODO: update session
+ // const newSession = await ctx.context.internalAdapter.createSession(
+ // session.user.id,
+ // );
+ // setSessionCookie(ctx, newSession.id);
+ return ctx.json(session.user);
+ },
+);
diff --git a/packages/better-auth/src/init.ts b/packages/better-auth/src/init.ts
index 85b123b347..ae309d2fca 100644
--- a/packages/better-auth/src/init.ts
+++ b/packages/better-auth/src/init.ts
@@ -70,6 +70,6 @@ export type AuthContext = {
};
password: {
hash: (password: string) => Promise;
- verify: (password: string, hash: string) => Promise;
+ verify: (hash: string, password: string) => Promise;
};
};