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; }; };